|
;; ess-tracebug.el --- Tracing and debugging facilities for ESS. -*- lexical-binding: t; -*-
|
|
;;
|
|
;; Copyright (C) 2011--2017 A.J. Rossini, Richard M. Heiberger, Martin Maechler,
|
|
;; Kurt Hornik, Rodney Sparapani, Stephen Eglen and Vitalie Spinu.
|
|
;;
|
|
;; Filename: ess-tracebug.el
|
|
;; Author: Vitalie Spinu
|
|
;; Maintainer: Vitalie Spinu
|
|
;; Copyright (C) 2010-2012, Vitalie Spinu, all rights reserved.
|
|
;; Created: Oct 14 14:15:22 2010
|
|
;; URL: https://code.google.com/p/ess-tracebug/
|
|
;; Keywords: tools, languages
|
|
;;
|
|
;; This file is *NOT* part of GNU Emacs.
|
|
;;
|
|
;; 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, 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.
|
|
;;
|
|
;; A copy of the GNU General Public License is available at
|
|
;; https://www.r-project.org/Licenses/
|
|
|
|
;;; Commentary:
|
|
;; Ess-tracebug is a package for interactive debugging of R code from
|
|
;; ESS and provides such features as:
|
|
;; - visual debugging
|
|
;; - browser, recover and conditional breakpoints
|
|
;; - watch window and loggers
|
|
;; - on the fly debug/undebug of R functions and methods
|
|
;; - highlighting of error source references and easy error navigation
|
|
;; - interactive traceback.
|
|
;;
|
|
;; For a complete description please see the documentation in the ESS
|
|
;; manual.
|
|
;;
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;
|
|
;;; Code:
|
|
|
|
(eval-when-compile
|
|
(when (< emacs-major-version 26)
|
|
(require 'cl))
|
|
(require 'cl-lib)
|
|
(require 'tramp)
|
|
(require 'subr-x))
|
|
(require 'comint)
|
|
(require 'compile)
|
|
(require 'ring)
|
|
(require 'ess-utils)
|
|
|
|
(defvar text-scale-mode-amount)
|
|
(autoload 'text-scale-mode "face-remap" "[autoload]" nil)
|
|
|
|
;; Silence the byte compiler. This is OK here because this file is
|
|
;; only loaded from ess-inf and has no autoloads.
|
|
;; TODO: This is a LOT. Can we move some of this around?
|
|
(defvar ess--dbg-del-empty-p)
|
|
(defvar inferior-ess-mode-map)
|
|
(defvar ess-mode-map)
|
|
(defvar ess--inhibit-presend-hooks)
|
|
(declare-function ess--accumulation-buffer "ess-inf")
|
|
(declare-function ess--if-verbose-write-process-state "ess-inf")
|
|
(declare-function ess--run-presend-hooks "ess-inf")
|
|
(declare-function ess-boolean-command "ess-inf")
|
|
(declare-function ess-build-eval-command "ess-inf")
|
|
(declare-function ess-build-load-command "ess-inf")
|
|
(declare-function ess-command "ess-inf")
|
|
(declare-function ess-dirs "ess-inf")
|
|
(declare-function ess-force-buffer-current "ess-inf")
|
|
(declare-function ess-get-process "ess-inf")
|
|
(declare-function ess-get-process-variable "ess-inf")
|
|
(declare-function ess-get-words-from-vector "ess-inf")
|
|
(declare-function ess-process-get "ess-inf")
|
|
(declare-function ess-process-live-p "ess-inf")
|
|
(declare-function ess-process-put "ess-inf")
|
|
(declare-function ess-send-string "ess-inf")
|
|
(declare-function ess-switch-process "ess-inf" ())
|
|
(declare-function ess-switch-to-ESS "ess-inf")
|
|
(declare-function ess-wait-for-process "ess-inf")
|
|
(declare-function ess-switch-to-end-of-ESS "ess-inf" ())
|
|
(declare-function ess-eval-region--normalise-region "ess-inf" )
|
|
(declare-function inferior-ess-run-callback "ess-inf")
|
|
(declare-function inferior-ess--set-status "ess-inf")
|
|
(declare-function ess-helpobjs-at-point--read-obj "ess-help")
|
|
(declare-function ess-r-get-evaluation-env "ess-r-mode")
|
|
(declare-function ess-r-package--all-source-dirs "ess-r-package")
|
|
(declare-function ess-r-package-name "ess-r-package")
|
|
(declare-function ess-r-package-source-dirs "ess-r-package")
|
|
|
|
;; Do not require tramp at runtime. It is expensive to load. Instead,
|
|
;; guard calls with (require 'tramp) and silence the byte compiler
|
|
;; here.
|
|
(declare-function tramp-dissect-file-name "tramp")
|
|
(declare-function tramp-get-remote-tmpdir "tramp")
|
|
;; 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")
|
|
|
|
|
|
(defgroup ess-tracebug nil
|
|
"Error navigation and debugging for ESS.
|
|
Currently only R is supported."
|
|
:link '(emacs-library-link :tag "Source Lisp File" "ess-tracebug.el")
|
|
:group 'ess)
|
|
|
|
(defvar ess-tracebug-indicator " TB"
|
|
"String to be displayed in mode-line alongside the process name.
|
|
Indicates that ess-tracebug-mode is turned on.")
|
|
|
|
(defvar ess-watch-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(define-key map "k" #'ess-watch-kill)
|
|
;; (define-key ess-watch-mode-map "u" #'ess-watch-undelete)
|
|
;; editing requires a little more work.
|
|
(define-key map "a" #'ess-watch-add)
|
|
(define-key map "i" #'ess-watch-insert)
|
|
(define-key map "e" #'ess-watch-edit-expression)
|
|
(define-key map "r" #'ess-watch-rename)
|
|
(define-key map "q" #'ess-watch-quit)
|
|
(define-key map "u" #'ess-watch-move-up)
|
|
(define-key map "U" #'ess-watch-move-down)
|
|
(define-key map "d" #'ess-watch-move-down)
|
|
(define-key map "n" #'ess-watch-next-block)
|
|
(define-key map "p" #'ess-watch-previous-block)
|
|
;; R mode keybindings.
|
|
(define-key map "\C-c\C-s" #'ess-switch-process)
|
|
(define-key map "\C-c\C-y" #'ess-switch-to-ESS)
|
|
(define-key map "\C-c\C-z" #'ess-switch-to-end-of-ESS)
|
|
map)
|
|
"Keymap for `ess-watch-mode'.")
|
|
|
|
|
|
(defcustom ess-tracebug-prefix nil
|
|
"Key to be used as prefix for all `ess-tracebug' commands.
|
|
Set this to a key combination you don't use often, like:
|
|
|
|
(setq ess-tracebug-prefix \"\\M-t\")
|
|
|
|
The postfix keys are defined in `ess-tracebug-map':
|
|
\\{ess-tracebug-map}"
|
|
:type '(choice (const nil) (string))
|
|
:group 'ess-tracebug)
|
|
|
|
(defcustom ess-tracebug-search-path nil
|
|
"List of directories to search for source files.
|
|
Elements should be directory names, not file names of directories."
|
|
:type '(choice (const :tag "Unset" nil)
|
|
(repeat :tag "Directory list" (string :tag "Directory")))
|
|
:group 'ess-debug)
|
|
|
|
(defvar ess-watch-buffer "*R watch*"
|
|
"Name of the watch buffer.")
|
|
|
|
(defcustom ess-watch-height-threshold nil
|
|
"Minimum height for splitting *R* window sensibly to make space for watch window.
|
|
See `split-height-threshold' for a detailed description.
|
|
|
|
If nil, the value of `split-height-threshold' is used."
|
|
:group 'ess-debug
|
|
:type '(choice (const nil) (integer)))
|
|
|
|
(defcustom ess-watch-width-threshold nil
|
|
"Minimum width for splitting *R* window sensibly to make space for watch window.
|
|
See `split-width-threshold' for a detailed description.
|
|
|
|
If nil, the value of `split-width-threshold' is used."
|
|
:group 'ess-debug
|
|
:type '(choice (const nil) (integer)))
|
|
|
|
(defcustom ess-watch-scale-amount -1
|
|
"The number of steps to scale the watch font down (up).
|
|
Each step scales the height of the default face in the watch
|
|
window by the variable `text-scale-mode-step' (a negative number
|
|
of steps decreases the height by the same amount)"
|
|
:group 'ess-debug
|
|
:type 'integer)
|
|
|
|
(defvar-local ess-watch-current-block-overlay nil
|
|
"The overlay for currently selected block in the R watch buffer .")
|
|
|
|
(defcustom ess-inject-source 'function-and-buffer
|
|
"Control the source injection into evaluated code.
|
|
|
|
If t, always inject source reference.
|
|
If function, inject only for functions,
|
|
If function-and-buffer, inject for functions and whole buffer (the default),
|
|
If nil, never inject.
|
|
|
|
When tracebug is active (the default), ESS instructs the
|
|
subprocess to keep the source code references.
|
|
|
|
If this variable is t, you won't be able to execute blocks which
|
|
don't form a valid R expression. That is, if your expression
|
|
spreads multiple paragraphs, and you call
|
|
\\[ess-eval-region-or-function-or-paragraph-and-step] on first
|
|
paragraph, R will report an error."
|
|
:group 'ess-tracebug
|
|
:type '(choice (const nil) (const function) (const function-and-buffer) (const t)))
|
|
|
|
(defcustom ess-tracebug-enter-hook nil
|
|
"List of functions to call on entry to `ess-tracebug' mode.
|
|
Use `add-hook' to insert append your functions to this list."
|
|
:group 'ess-tracebug
|
|
:type 'hook)
|
|
|
|
(defcustom ess-tracebug-exit-hook nil
|
|
"List of functions to call on exit of `ess-tracebug' mode.
|
|
Use `add-hook' to insert append your functions to this list."
|
|
:group 'ess-tracebug
|
|
:type 'hook)
|
|
|
|
(defvaralias 'ess-tracebug-map 'ess-dev-map)
|
|
|
|
(defvar ess--tracebug-eval-index 0
|
|
"This is used by to track source references in evaluation with source.
|
|
For example, each time `ess-eval-function' is called the evaluated
|
|
region is marked. When debugger enters the code it displays
|
|
this reference number. Ess-debug finds this number in the
|
|
referenced buffer.")
|
|
|
|
;; these vars are org variables that store the src block locations
|
|
(defvar org-edit-src-beg-marker nil)
|
|
(defvar org-babel-current-src-block-location nil
|
|
"Marker pointing to the src block currently being executed.
|
|
This may also point to a call line or an inline code block. If
|
|
multiple blocks are being executed (e.g., in chained execution
|
|
through use of the :var header argument) this marker points to
|
|
the outer-most code block.")
|
|
|
|
;; hash to store source references of the form: tmpname -> (filename . src_start)
|
|
(defvar ess--srcrefs (make-hash-table :test 'equal :size 100))
|
|
|
|
(defvar ess-tracebug-original-buffer-marker nil
|
|
"Marker pointing to the beginning of original source code.
|
|
If non-nil, tracebug will insert the source references based on
|
|
this location instead of the current buffer. This is useful for
|
|
applications, like org-babel, that call ess evaluation functions
|
|
from temporary buffers.")
|
|
|
|
(defun ess-tracebug-p ()
|
|
"Return non-nil if tracebug is running."
|
|
(ess-process-get 'tracebug))
|
|
|
|
(defun ess-make-source-refd-command (beg end visibly process)
|
|
"Saves a region to a temporary file in order to add source references.
|
|
BEG and END delimit the region. Returns a string containing an
|
|
inferior process command for loading the temporary file. This
|
|
command conforms to VISIBLY."
|
|
(let* ((filename buffer-file-name)
|
|
(proc-dir (ess-get-process-variable 'default-directory))
|
|
(remote (when (file-remote-p proc-dir)
|
|
(require 'tramp)
|
|
;; should this be done in process buffer?
|
|
(tramp-dissect-file-name proc-dir)))
|
|
(orig-marker (or ess-tracebug-original-buffer-marker
|
|
org-edit-src-beg-marker
|
|
org-babel-current-src-block-location))
|
|
orig-beg)
|
|
(setq ess--tracebug-eval-index (1+ ess--tracebug-eval-index))
|
|
(goto-char beg)
|
|
(skip-chars-forward " \t\n")
|
|
(setq beg (point))
|
|
(goto-char end)
|
|
(skip-chars-backward " \t\n")
|
|
(setq end (point)
|
|
orig-beg beg)
|
|
|
|
;; Delete all old temp files
|
|
(when (and (not (ess-process-get 'busy))
|
|
(< 1 (float-time
|
|
(time-subtract (current-time)
|
|
(ess-process-get 'last-eval)))))
|
|
(dolist (f (ess-process-get 'temp-source-files))
|
|
(and (file-exists-p f)
|
|
(delete-file f)))
|
|
(ess-process-put 'temp-source-files nil))
|
|
|
|
(when (markerp orig-marker)
|
|
(setq filename (buffer-file-name (marker-buffer orig-marker)))
|
|
(setq orig-beg (+ beg (marker-position orig-marker))))
|
|
|
|
(let ((tmpfile
|
|
(expand-file-name (make-temp-name
|
|
(concat (file-name-nondirectory
|
|
(or filename "unknown")) "!"))
|
|
(if remote
|
|
(tramp-get-remote-tmpdir remote)
|
|
temporary-file-directory))))
|
|
|
|
(ess-process-put 'temp-source-files
|
|
(cons tmpfile (ess-process-get 'temp-source-files)))
|
|
|
|
(when remote
|
|
;; Get local name (should this be done in process buffer?)
|
|
(setq tmpfile (with-parsed-tramp-file-name tmpfile nil localname)))
|
|
|
|
(if (not filename)
|
|
(puthash tmpfile (list nil ess--tracebug-eval-index nil) ess--srcrefs)
|
|
(puthash tmpfile (list filename ess--tracebug-eval-index orig-beg) ess--srcrefs)
|
|
(puthash (file-name-nondirectory tmpfile) ; R sometimes strips dirs
|
|
(list filename ess--tracebug-eval-index orig-beg) ess--srcrefs)
|
|
(with-silent-modifications
|
|
(put-text-property beg end 'tb-index ess--tracebug-eval-index)))
|
|
(let ((string (ess-process-buffer-substring process beg end)))
|
|
(or
|
|
;; Sending string to subprocess is considerably faster than tramp file
|
|
;; transfer. So, give priority to `ess-eval-command' if available
|
|
(ess-build-eval-command string visibly t tmpfile)
|
|
;; When no `ess-eval-command' available, use `ess-load-command'
|
|
(progn
|
|
(write-region beg end tmpfile nil 'silent)
|
|
(ess-build-load-command tmpfile visibly t)))))))
|
|
|
|
(defun ess-process-buffer-substring (process start end)
|
|
(ess--run-presend-hooks process (buffer-substring-no-properties start end)))
|
|
|
|
(defun ess-tracebug-send-region (process start end &optional visibly message type)
|
|
"Send region to process adding source references as specified
|
|
by `ess-inject-source' variable."
|
|
(ess-eval-region--normalise-region start end)
|
|
(let* ((inject-p (cond ((eq type 'function)
|
|
ess-inject-source)
|
|
((eq type 'buffer)
|
|
(or (eq ess-inject-source t)
|
|
(eq ess-inject-source 'function-and-buffer)))
|
|
(t (or (eq ess-inject-source t)
|
|
;; We need to always inject with namespaced
|
|
;; evaluation (fixme: not right place for
|
|
;; this).
|
|
(ess-r-get-evaluation-env)))))
|
|
(ess--dbg-del-empty-p (unless inject-p ess--dbg-del-empty-p))
|
|
(string (if inject-p
|
|
(ess-make-source-refd-command start end visibly process)
|
|
(ess-process-buffer-substring process start end)))
|
|
(message (if (fboundp ess-build-eval-message-function)
|
|
(funcall ess-build-eval-message-function message)
|
|
message)))
|
|
;; Don't run the presend hooks twice.
|
|
(let ((ess--inhibit-presend-hooks t))
|
|
(process-put process :eval-visibly visibly)
|
|
;; Visible evaluation is not nice when sourcing temporary files. You get
|
|
;; .ess.eval(*code*) instead of *code*.
|
|
(setq visibly (unless inject-p visibly))
|
|
(ess-send-string process string visibly message))))
|
|
|
|
(defun ess-tracebug-send-function (proc start end &optional visibly message)
|
|
"Like `ess-tracebug-send-region' but with tweaks for functions."
|
|
(ess-tracebug-send-region proc start end visibly message 'function))
|
|
|
|
(defvar ess-tracebug-help nil
|
|
"ess-dev-map prefix: \\[ess-dev-map]
|
|
|
|
* Breakpoints (`ess-dev-map'):
|
|
|
|
b . Set BP (repeat to cycle BP type) . `ess-bp-set'
|
|
B . Set conditional BP . `ess-bp-set-conditional'
|
|
k . Kill BP . `ess-bp-kill'
|
|
K . Kill all BPs . `ess-bp-kill-all'
|
|
o . Toggle BP state . `ess-bp-toggle-state'
|
|
l . Set logger BP . `ess-bp-set-logger'
|
|
n . Goto next BP . `ess-bp-next'
|
|
p . Goto previous BP . `ess-bp-previous'
|
|
|
|
(C- prefixed equivalents are also defined)
|
|
|
|
* Debugging (`ess-dev-map'):
|
|
` . Show traceback . `ess-show-traceback' (also on C-c `)
|
|
~ . Show callstack . `ess-show-call-stack' (also on C-c ~)
|
|
e . Toggle error action (repeat to cycle). `ess-debug-toggle-error-action'
|
|
d . Flag for debugging . `ess-debug-flag-for-debugging'
|
|
u . Unflag for debugging . `ess-debug-unflag-for-debugging'
|
|
w . Watch window . `ess-watch'
|
|
|
|
(C- prefixed equivalents are also defined)
|
|
|
|
* Interactive Debugging (`ess-debug-minor-mode-map'):
|
|
|
|
M-C . Continue . `ess-debug-command-continue'
|
|
M-C-C . Continue multi . `ess-debug-command-continue-multi'
|
|
M-N . Next step . `ess-debug-command-next'
|
|
M-C-N . Next step multi . `ess-debug-command-next-multi'
|
|
M-U . Up frame . `ess-debug-command-up'
|
|
M-Q . Quit debugging . `ess-debug-command-quit'
|
|
|
|
* Navigation to errors (general Emacs functionality):
|
|
|
|
C-x `, M-g n . `next-error'
|
|
M-g p . `previous-error'")
|
|
|
|
|
|
;; * Input Ring:
|
|
|
|
;; i . Goto input event marker forwards . `ess-debug-goto-input-event-marker'
|
|
;; I . Goto input event marker backwards . `ess-debug-goto-input-event-marker'
|
|
|
|
|
|
(defun ess-tracebug-show-help ()
|
|
"Show help for `ess-tracebug'."
|
|
(interactive)
|
|
(describe-variable 'ess-tracebug-help))
|
|
|
|
(defun ess-tracebug--propertize (dummy bitmap face &optional string )
|
|
"If `window-system' propertize DUMMY with fringe BITMAP and FACE.
|
|
Otherwise, propertize line-prefix and margin with STRING and FACE"
|
|
(unless string
|
|
(setq string dummy))
|
|
(if window-system
|
|
(propertize dummy 'display (list 'left-fringe bitmap face))
|
|
(propertize dummy
|
|
'display (list '(margin left-margin)
|
|
(propertize string
|
|
'font-lock-face face
|
|
'face face)))))
|
|
|
|
|
|
(defun ess-tracebug (&optional arg)
|
|
"Toggle `ess-tracebug' mode.
|
|
With ARG, turn `ess-tracebug' mode on if and only if ARG is
|
|
positive.
|
|
|
|
This mode adds to ESS the interactive debugging, breakpoint and
|
|
error navigation functionality. Strictly speaking `ess-tracebug'
|
|
is not a minor mode. It integrates globally into ESS and iESS.
|
|
|
|
Note: Currently, `ess-tracebug' does not detect some of R's debug
|
|
related messages in non-English locales. To set your R messages
|
|
to English add the following line to your .Rprofile init file:
|
|
|
|
Sys.setlocale(\"LC_MESSAGES\", \"C\")
|
|
|
|
|
|
See `ess-tracebug-help' for the overview of ess-tracebug functionality."
|
|
|
|
;; Note: The functionality in ess-tracebug is divided on conceptual
|
|
;; grounds in tracing and debugging and could be
|
|
;; activated/deactivate separately with `ess--tb-start' and
|
|
;; `ess-debug-start' respectively.
|
|
|
|
(interactive "P")
|
|
(ess-force-buffer-current "R process to activate tracebug in: ")
|
|
(with-current-buffer (process-buffer (get-process ess-local-process-name))
|
|
(when (equal ess-dialect "R")
|
|
(setq arg
|
|
(if arg
|
|
(prefix-numeric-value arg)
|
|
(if (ess-process-get 'tracebug) -1 1)))
|
|
(if (> arg 0)
|
|
(unless (ess-process-get 'tracebug) ;; only if already not active
|
|
(ess--tb-start)
|
|
(ess-debug-start)
|
|
;; (dolist (bf (buffer-list))
|
|
;; (with-current-buffer bf
|
|
;; (when (and (eq major-mode 'ess-mode)
|
|
;; (equal ess-dialect "R"))
|
|
;; (ess-bp-recreate-all))))
|
|
;; watch functionality
|
|
(if ess-tracebug-prefix
|
|
(let ((comm (key-binding ess-tracebug-prefix)))
|
|
;; (message "ess-tracebug-prefix will be removed in future versions. Electric debug keys are now on [C-c] and [C-c C-t] maps.")
|
|
;; (sit-for 1)
|
|
(when (commandp comm)
|
|
(define-key ess-tracebug-map ess-tracebug-prefix comm))
|
|
(define-key ess-mode-map ess-tracebug-prefix ess-tracebug-map)
|
|
(define-key inferior-ess-mode-map ess-tracebug-prefix ess-tracebug-map)
|
|
(define-key ess-watch-mode-map ess-tracebug-prefix ess-tracebug-map)))
|
|
(run-hooks 'ess-tracebug-enter-hook)
|
|
(ess-process-put 'tracebug t)
|
|
(message "ess-tracebug mode enabled"))
|
|
(when (ess-process-get 'tracebug) ;;only when active
|
|
(ess-process-put 'tracebug nil)
|
|
;; unset the map
|
|
(when ess-tracebug-prefix
|
|
(define-key ess-mode-map ess-tracebug-prefix nil)
|
|
(define-key inferior-ess-mode-map ess-tracebug-prefix nil))
|
|
(ess--tb-stop)
|
|
(ess-debug-stop)
|
|
(run-hooks 'ess-tracebug-exit-hook)
|
|
(message "ess-tracebug mode disabled"))))))
|
|
|
|
(defalias 'ess-toggle-tracebug 'ess-tracebug)
|
|
|
|
|
|
;;;_* TRACEBACK
|
|
|
|
;; (defface ess--tb-last-input-face
|
|
;; '((((class grayscale)
|
|
;; (background light)) (:background "DimGray"))
|
|
;; (((class grayscale)
|
|
;; (background dark)) (:background "LightGray"))
|
|
;; (((class color) (background light) (min-colors 88))
|
|
;; (:overline "medium blue" ))
|
|
;; (((class color) (background dark) (min-colors 88))
|
|
;; (:overline "deep sky blue" ))
|
|
;; (((background light)) (:weight bold))
|
|
;; (((background dark)) (:weight bold))
|
|
;; )
|
|
;; "Face to highlight currently debugged line."
|
|
;; :group 'ess-tracebug )
|
|
|
|
(defface ess-tracebug-last-input-fringe-face
|
|
'((((background light) (min-colors 88)) (:foreground "medium blue" :overline "medium blue"))
|
|
(((background dark) (min-colors 88)) (:foreground "deep sky blue" :overline "deep sky blue"))
|
|
(((background light) (min-colors 8)) (:foreground "blue"))
|
|
(((background dark) (min-colors 8)) (:foreground "syan")))
|
|
"Face for fringe bitmap for last-input position."
|
|
:group 'ess-tracebug)
|
|
|
|
(if (fboundp 'define-fringe-bitmap)
|
|
(define-fringe-bitmap 'last-input-arrow
|
|
[#b00011111
|
|
#b00010000
|
|
#b00010000
|
|
#b00010000
|
|
#b00010000
|
|
#b00010000
|
|
#b00010000
|
|
#b00010000
|
|
#b00010000
|
|
#b00010000
|
|
#b11010111
|
|
#b01111100
|
|
#b00111000
|
|
#b00010000] nil nil 'top))
|
|
|
|
|
|
(defvar ess--tb-last-input (make-marker)
|
|
"Marker pointing to the last user input position in iESS buffer.
|
|
This is the place where `ess--tb-last-input-overlay' is moved.
|
|
Local in iESS buffers with `ess-tracebug' mode enabled.")
|
|
|
|
(defvar ess--tb-last-input-overlay nil
|
|
"Overlay to highlight the position of last input in iESS buffer.
|
|
Local in iESS buffers.")
|
|
|
|
(defvar-local ess--busy-count 0
|
|
"Used to compute the busy indicator.")
|
|
|
|
;; (unless (boundp 'ess--busy-slash)
|
|
;; (defvar ess--busy-slash '(32 ?\u2014 92 47))
|
|
;; (setq ess--busy-slash (mapcar (lambda (el) (format " %c " el))
|
|
;; ess--busy-slash))
|
|
;; )
|
|
|
|
|
|
(defvar ess--busy-slash '(" " " - " " \\ " " / "))
|
|
(defvar ess--busy-B '(" " " B " " "))
|
|
(defvar ess--busy-stars '(" " " " " * " " ** " " *** " " **** "))
|
|
(defvar ess--busy-vbars '(" " " " " | " " || " " ||| " " |||| "))
|
|
|
|
(defcustom ess-busy-strings ess--busy-slash
|
|
"List of strings to replace in turn for busy indication.
|
|
The first element of the list is used as an indicator of the
|
|
process being ready (i.e. not busy). Implemented lists that you
|
|
can use `ess--busy-slash', `ess--busy-B',`ess--busy-stars',
|
|
`ess--busy-vbars'"
|
|
:group 'ess
|
|
:type '(repeat string))
|
|
|
|
(defvar ess--busy-timer nil
|
|
"Timer used for busy process indication.")
|
|
|
|
(defcustom inferior-ess-replace-long+ t
|
|
"Determines if ESS replaces long + sequences in output.
|
|
If 'strip, remove all such instances. Otherwise, if non-nil, '+
|
|
+ + + ' containing more than 4 + is replaced by
|
|
`ess-long+replacement'."
|
|
:group 'ess-tracebug
|
|
:type '(choice (const nil :tag "No replacement")
|
|
(const 'strip :tag "Replace all")
|
|
(const t :tag "Replace 4 or more +")))
|
|
|
|
(defvar ess-long+replacement ". + "
|
|
"Replacement used for long + prompt.
|
|
Please don't customize this or other prompt related variables.
|
|
ESS internal code assumes default R prompts.")
|
|
|
|
(defmacro ess-copy-key (from-map to-map fun)
|
|
`(define-key ,to-map
|
|
(car (where-is-internal ,fun ,from-map))
|
|
,fun))
|
|
|
|
;;;_ + traceback functions
|
|
(defun ess--tb-make-last-input-overlay (beg end)
|
|
"Create an overlay to indicate the last input position."
|
|
(let ((ove (make-overlay beg end)))
|
|
(overlay-put ove 'before-string
|
|
(ess-tracebug--propertize "!" 'last-input-arrow 'ess-tracebug-last-input-fringe-face))
|
|
;; (overlay-put ove 'face 'ess--tb-last-input-face)
|
|
(overlay-put ove 'evaporate t)
|
|
ove))
|
|
|
|
|
|
(defun ess--tb-start ()
|
|
"Start traceback session."
|
|
(with-current-buffer (process-buffer (get-process ess-local-process-name))
|
|
(unless ess-error-regexp-alist
|
|
(error "Can not activate the traceback for %s dialect" ess-dialect))
|
|
(setq-local compilation-error-regexp-alist ess-error-regexp-alist)
|
|
(let (compilation-mode-font-lock-keywords)
|
|
(compilation-setup t))
|
|
(setq next-error-function 'ess-tracebug-next-error-function)
|
|
;; new locals
|
|
(make-local-variable 'ess--tb-last-input)
|
|
(make-local-variable 'ess--tb-last-input-overlay)
|
|
(make-local-variable 'compilation-search-path)
|
|
(setq compilation-search-path ess-tracebug-search-path) ;; TODO: make this dialect specific
|
|
(ess-tracebug--set-left-margin)
|
|
(save-excursion
|
|
(goto-char comint-last-input-start)
|
|
(setq ess--tb-last-input (point))
|
|
(setq ess--tb-last-input-overlay
|
|
(ess--tb-make-last-input-overlay
|
|
(point-at-bol) (point-at-eol))))
|
|
;; busy timer
|
|
(setq mode-line-buffer-identification
|
|
(list (car (propertized-buffer-identification "%3b"))
|
|
`(:eval (nth ess--busy-count ess-busy-strings)))) ;; 'face 'mode-line-buffer-id))))
|
|
(make-local-variable 'ess--busy-timer)
|
|
(setq ess--busy-timer
|
|
(run-with-timer 2 .5 (ess--make-busy-timer-function (get-buffer-process (current-buffer)))))
|
|
(add-hook 'kill-buffer-hook (lambda () (when ess--busy-timer (cancel-timer ess--busy-timer))))
|
|
(add-hook 'comint-input-filter-functions 'ess-tracebug-set-last-input nil 'local)
|
|
|
|
;; redefine
|
|
;; TODO: all this part should go (partially gone now)
|
|
(unless (fboundp 'orig-ess-parse-errors)
|
|
(defalias 'orig-ess-parse-errors (symbol-function 'ess-parse-errors))
|
|
(defalias 'ess-parse-errors (symbol-function 'next-error)))))
|
|
|
|
(defun ess--tb-stop ()
|
|
"Stop ess traceback session in the current ess process."
|
|
(with-current-buffer (process-buffer (get-process ess-current-process-name))
|
|
;; restore original definitions
|
|
(when (equal ess-dialect "R")
|
|
(when (fboundp 'orig-ess-parse-errors)
|
|
(defalias 'ess-parse-errors (symbol-function 'orig-ess-parse-errors))
|
|
(fmakunbound 'orig-ess-parse-errors)))
|
|
(if (local-variable-p 'ess--tb-last-input-overlay)
|
|
(delete-overlay ess--tb-last-input-overlay))
|
|
(kill-local-variable 'ess--tb-last-input-overlay)
|
|
(kill-local-variable 'ess--tb-last-input)
|
|
(font-lock-remove-keywords nil (compilation-mode-font-lock-keywords))
|
|
(font-lock-ensure)
|
|
(kill-local-variable 'compilation-error-regexp-alist)
|
|
(kill-local-variable 'compilation-search-path)
|
|
(cancel-timer ess--busy-timer)
|
|
(remove-hook 'comint-input-filter-functions 'ess-tracebug-set-last-input 'local)
|
|
(setq mode-line-buffer-identification (propertized-buffer-identification "%12b"))))
|
|
|
|
(defvar ess--dbg-forward-ring (make-ring 10)
|
|
"Ring of markers to the positions of user inputs when the
|
|
debugger or traceback events are initiated. It is used in
|
|
`ess--dbg-goto-input-point'.")
|
|
|
|
(defvar ess--dbg-backward-ring (make-ring 10)
|
|
"Ring of markers to the positions from which `ess--dbg-goto-input-point' is called.
|
|
See the also `ess--dbg-goto-debug-point'")
|
|
|
|
;; (setq ess-R--tb-regexp-alist '(R R2 R3 R-recover))
|
|
;;(pop compilation-error-regexp-alist-alist)
|
|
|
|
(defun ess-show-traceback ()
|
|
"Display R traceback and last error message.
|
|
Pop up a compilation/grep/occur like buffer. Usual global key
|
|
bindings are available (\\[next-error] and \\[previous-error])
|
|
for `next-error' and `previous-error' respectively.
|
|
|
|
You can bind 'no-select' versions of this commands:
|
|
\(define-key compilation-minor-mode-map [(?n)] #'next-error-no-select)
|
|
\(define-key compilation-minor-mode-map [(?p)] #'previous-error-no-select)"
|
|
(interactive)
|
|
(cl-assert ess-traceback-command nil
|
|
"Not implemented for dialect %s" ess-dialect)
|
|
(ring-insert ess--dbg-forward-ring (point-marker))
|
|
(ess-force-buffer-current "R process to use: ")
|
|
(let ((trbuf (get-buffer-create "*ess-traceback*"))
|
|
(lproc-name ess-local-process-name)
|
|
(alist ess-mode-editing-alist)
|
|
(cmd ess-traceback-command)
|
|
(inhibit-read-only t))
|
|
(setq next-error-last-buffer trbuf)
|
|
(with-current-buffer trbuf
|
|
(setq ess-local-process-name lproc-name)
|
|
(ess-command cmd trbuf)
|
|
(goto-char (point-min))
|
|
;; fixme: this is R specific check
|
|
(cl-assert (not (re-search-forward "No traceback available" nil t)) nil
|
|
"No traceback available")
|
|
(ess-dirs)
|
|
(when (boundp 'ess-r-error-regexp-alist)
|
|
(setq-local compilation-error-regexp-alist ess-r-error-regexp-alist))
|
|
(setq-local compilation-search-path ess-tracebug-search-path)
|
|
(ess-setq-vars-local alist)
|
|
(font-lock-refresh-defaults)
|
|
(compilation-minor-mode 1)
|
|
(setq next-error-function #'ess-tracebug-next-error-function)
|
|
(setq buffer-read-only t)
|
|
(pop-to-buffer trbuf))))
|
|
|
|
(defvar ess-call-stack-command nil)
|
|
(defun ess-show-call-stack ()
|
|
"Display current call stack.
|
|
Also see `ess-show-traceback'"
|
|
(interactive)
|
|
(let ((ess-traceback-command ess-call-stack-command))
|
|
(ess-show-traceback)))
|
|
|
|
(defalias 'ess-show-R-traceback 'ess-show-traceback)
|
|
|
|
(defun ess--tb-next-error-goto-process-marker ()
|
|
;; assumes current buffer is the process buffer with compilation enabled
|
|
;; used in ess-tracebug-next-error-function
|
|
; (with-current-buffer (process-buffer (get-process ess-local-process-name)) ; already in comint buffer .. no need
|
|
(comint-goto-process-mark)
|
|
(set-window-point (get-buffer-window) (point)) ;moves the cursor
|
|
;; FIXME: Should jump to current-debug-position, but messes the things if in recover
|
|
;; (when (ess-debug-is-active)
|
|
;; (ess-debug-goto-current-debug-position)
|
|
;; )
|
|
)
|
|
|
|
(defun ess-tracebug-next-error-function (n &optional reset)
|
|
"Advance to the next error message and visits the file.
|
|
This is the value of `next-error-function' in iESS buffers."
|
|
;; Modified version of `compilation-next-error-function'.
|
|
(interactive "p")
|
|
(if reset (goto-char (point-max)))
|
|
(let* (;; (columns compilation-error-screen-columns) ; buffer's local value
|
|
;; (proc (or (get-buffer-process (current-buffer))
|
|
;; (error "Current buffer has no process")))
|
|
(pbuff-p (get-buffer-process (current-buffer)))
|
|
(n (or n 1))
|
|
(beg-pos ; from where the search for next error starts
|
|
(if (and pbuff-p
|
|
(>= n 0)
|
|
(comint-after-pmark-p))
|
|
ess--tb-last-input
|
|
(point)))
|
|
(at-error t)
|
|
(msg
|
|
(condition-case nil
|
|
(compilation-next-error n nil beg-pos)
|
|
(error
|
|
(when pbuff-p
|
|
(ess--tb-next-error-goto-process-marker))
|
|
(if (< n 0)
|
|
(message "Before first reference")
|
|
(message "Beyond last reference"));(error-message-string err))
|
|
(setq at-error nil))))
|
|
(msg (if (or (not pbuff-p)
|
|
(eq n 0)
|
|
(> (point) ess--tb-last-input))
|
|
msg
|
|
(ess--tb-next-error-goto-process-marker)
|
|
(message "Beyond last-input marker")
|
|
(setq at-error nil)))
|
|
(marker (point-marker))
|
|
loc)
|
|
(when at-error
|
|
(setq compilation-current-error (point-marker)
|
|
overlay-arrow-position (if (bolp)
|
|
compilation-current-error
|
|
(copy-marker (line-beginning-position)))
|
|
loc (if (fboundp 'compilation--message->loc)
|
|
(compilation--message->loc msg)
|
|
(car msg)))
|
|
(let* ((file (caar (nth 2 loc)))
|
|
(col (car loc))
|
|
(line (cadr loc))
|
|
(mkrs (ess--dbg-create-ref-marker file line col)))
|
|
(if mkrs
|
|
;; is this really needed? Shall we go directly to the location?
|
|
(compilation-goto-locus marker (car mkrs) (cadr mkrs))
|
|
(message "Reference to '%s' not found" file))))))
|
|
|
|
|
|
(defun inferior-ess-move-last-input-overlay ()
|
|
"Move the overlay to the point."
|
|
(let ((pbol (point-at-bol)))
|
|
(move-overlay ess--tb-last-input-overlay
|
|
pbol (max (- (point) 2) (+ pbol 2)))))
|
|
|
|
|
|
;;;_* DEBUGGER
|
|
(defgroup ess-debug nil
|
|
"Debugging for ESS"
|
|
:link '(emacs-library-link :tag "Source Lisp File" "ess-tracebug.el")
|
|
:group 'ess-tracebug
|
|
:prefix "ess-debug-")
|
|
|
|
(defcustom ess-debug-error-action-alist
|
|
'(( "" "NONE" "NULL" )
|
|
( " r" "RECOVER" "utils::recover")
|
|
( " t" "TRACEBACK" "base::traceback"))
|
|
"Alist of 'on-error' actions.
|
|
Toggled with `ess-debug-toggle-error-action'. Each element must
|
|
have the form (DISP SYMB ACTION) where DISP is the string to be
|
|
displayed in the mode line when the action is in place. SYMB is
|
|
the symbolic name of an action. ACTION is the string giving the
|
|
actual expression to be assigned to 'error' user option. See R's
|
|
help ?options for more details."
|
|
:type '(alist :key-type string
|
|
:value-type (group string string))
|
|
:group 'ess-debug)
|
|
|
|
(defvar ess--dbg-output-buf-prefix " *ess.dbg"
|
|
"The prefix of the buffer name the R debug output is directed to." )
|
|
|
|
(defvar-local ess--dbg-current-ref (make-marker)
|
|
"Current debug reference in *ess.dbg* buffers (a marker).")
|
|
|
|
(defvar-local ess--dbg-last-ref-marker (make-marker)
|
|
"Last debug reference in *ess.dbg* buffer (a marker).")
|
|
|
|
(defvar-local ess--dbg-buf-p nil
|
|
"This is t in ess.dbg buffers.")
|
|
|
|
;; (defcustom ess--dbg-auto-single-key-p t
|
|
;; "If t entering the debug state triggers single-key mode.
|
|
;; Set it to nil if you want to trigger single-key mode manually
|
|
;; with the `ess-tracebug-prefix' key.
|
|
;; ")
|
|
|
|
(defvar ess--dbg-current-debug-position (make-marker)
|
|
"Marker to the current debugged line.
|
|
It always point to the beginning of the currently debugged line
|
|
and is used by overlay-arrow.
|
|
In no-windowed Emacs an `overlay-arrow' is displayed at this position.")
|
|
|
|
(unless window-system
|
|
(add-to-list 'overlay-arrow-variable-list 'ess--dbg-current-debug-position))
|
|
|
|
(defface ess-debug-current-debug-line-face
|
|
'((default (:inherit highlight)))
|
|
"Face used to highlight currently debugged line."
|
|
:group 'ess-debug)
|
|
|
|
|
|
(defvar ess--dbg-current-debug-overlay
|
|
(let ((overlay (make-overlay (point) (point))))
|
|
(overlay-put overlay 'face 'ess-debug-current-debug-line-face)
|
|
(overlay-put overlay 'evaporate t)
|
|
overlay)
|
|
;; should be global variable!!
|
|
"The overlay for currently debugged line.")
|
|
|
|
|
|
(defcustom ess-debug-blink-interval .2
|
|
"Time in seconds to blink the background of the debug line.
|
|
Currently two events are defined 'ref-not-found' and 'same-ref'.
|
|
Blinking colors for these events can be customized by
|
|
corresponding faces."
|
|
:group 'ess-debug
|
|
:type 'float)
|
|
|
|
(defface ess-debug-blink-ref-not-found-face
|
|
'((((class grayscale) (background light)) (:background "DimGray"))
|
|
(((class grayscale) (background dark)) (:background "LightGray"))
|
|
(((class color) (background light) (min-colors 88)) (:background "IndianRed4"))
|
|
(((class color) (background dark) (min-colors 88)) (:background "dark red"))
|
|
(((background light) (min-colors 8)) (:foreground "red"))
|
|
(((background dark) (min-colors 8)) (:foreground "red")))
|
|
"Face used to blink currently debugged line's background
|
|
when the reference file is not found. See also `ess-debug-ask-for-file'"
|
|
:group 'ess-debug )
|
|
|
|
(defface ess-debug-blink-same-ref-face
|
|
'((((class grayscale) (background light)) (:background "DimGray"))
|
|
(((class grayscale) (background dark)) (:background "LightGray"))
|
|
(((class color) (background light) (min-colors 88)) (:background "steel blue"))
|
|
(((class color) (background dark) (min-colors 88)) (:background "midnight blue"))
|
|
(((background light) (min-colors 8)) (:foreground "blue"))
|
|
(((background dark) (min-colors 8)) (:foreground "cyan")))
|
|
"Face used to highlight currently debugged line when new debug
|
|
reference is the same as the preceding one. It is highlighted for
|
|
`ess-debug-blink-interval' seconds."
|
|
:group 'ess-debug )
|
|
|
|
(defcustom ess-debug-ask-for-file nil
|
|
"If non nil, ask for file if the current debug reference is not found.
|
|
|
|
If nil, the currently debugged line is highlighted for
|
|
`ess-debug-blink-interval' seconds."
|
|
:group 'ess-debug
|
|
:type 'boolean)
|
|
|
|
(defcustom ess-debug-skip-first-call t
|
|
"If non-nil, skip first debugger call.
|
|
|
|
In R first call doesn't contain source references and is skipped
|
|
by default."
|
|
:group 'ess-debug
|
|
:type 'boolean)
|
|
|
|
(defvar ess-electric-selection-map
|
|
(let (ess-electric-selection-map)
|
|
(define-prefix-command 'ess-electric-selection-map)
|
|
;; command-c and command-Q are not always working reliably
|
|
(define-key ess-electric-selection-map "\M-N" #'ess-debug-command-continue)
|
|
(define-key ess-electric-selection-map "\M-C" #'ess-debug-command-continue)
|
|
(define-key ess-electric-selection-map "\M-Q" #'ess-debug-command-quit)
|
|
(define-key ess-electric-selection-map "0" #'ess-debug-command-digit)
|
|
(define-key ess-electric-selection-map "1" #'ess-debug-command-digit)
|
|
(define-key ess-electric-selection-map "2" #'ess-debug-command-digit)
|
|
(define-key ess-electric-selection-map "3" #'ess-debug-command-digit)
|
|
(define-key ess-electric-selection-map "4" #'ess-debug-command-digit)
|
|
(define-key ess-electric-selection-map "5" #'ess-debug-command-digit)
|
|
(define-key ess-electric-selection-map "6" #'ess-debug-command-digit)
|
|
(define-key ess-electric-selection-map "7" #'ess-debug-command-digit)
|
|
(define-key ess-electric-selection-map "8" #'ess-debug-command-digit)
|
|
(define-key ess-electric-selection-map "9" #'ess-debug-command-digit)
|
|
(define-key ess-electric-selection-map "?" #'ess-tracebug-show-help)
|
|
ess-electric-selection-map)
|
|
"Keymap used to define commands for single key input mode.
|
|
This commands are triggered by `ess-electric-selection' .
|
|
|
|
\\{ess-electric-selection-map}")
|
|
|
|
;;;_ + debug functions
|
|
(defun ess-debug-set-error-action (spec)
|
|
"Set the on-error action.
|
|
The SPEC should be one of the components of
|
|
`ess-debug-error-action-alist'."
|
|
(let ((proc (get-process ess-local-process-name)))
|
|
(if spec
|
|
(with-current-buffer (process-buffer proc)
|
|
(process-put proc 'on-error-action (car spec))
|
|
(ess-command (format "options(error= %s )\n" (nth 2 spec))))
|
|
(error "Unknown action"))))
|
|
|
|
(defun ess-debug-toggle-error-action ()
|
|
"Toggle the 'on-error' action.
|
|
The action list is in `ess-debug-error-action-alist'."
|
|
(interactive)
|
|
(ess-force-buffer-current)
|
|
(let* ((ev last-command-event)
|
|
(com-char (event-basic-type ev))
|
|
(cur-action (or (ess-process-get 'on-error-action) ""))
|
|
actions act)
|
|
(setq actions
|
|
(cdr (member (assoc cur-action ess-debug-error-action-alist)
|
|
ess-debug-error-action-alist)))
|
|
(unless actions
|
|
(setq actions ess-debug-error-action-alist))
|
|
(setq act (pop actions))
|
|
(ess-debug-set-error-action act)
|
|
(message "On-error action set to: %s"
|
|
(propertize (cadr act) 'face 'font-lock-function-name-face))
|
|
(while (eq (event-basic-type (setq ev (read-event))) com-char)
|
|
(unless actions
|
|
(setq actions ess-debug-error-action-alist))
|
|
(setq act (pop actions))
|
|
(ess-debug-set-error-action act)
|
|
(force-mode-line-update)
|
|
(message "On-error action set to: %s"
|
|
(propertize (cadr act) 'face 'font-lock-function-name-face)))
|
|
(push ev unread-command-events)))
|
|
|
|
(defun ess--dbg-activate-overlays ()
|
|
"Initialize active debug line overlays."
|
|
(move-overlay ess--dbg-current-debug-overlay
|
|
(point-at-bol) (1+ (point-at-eol)) (current-buffer))
|
|
;; used by overlay-arrow functionality on no-X, should be bol
|
|
(move-marker ess--dbg-current-debug-position (point-at-bol)))
|
|
|
|
(defun ess--dbg-deactivate-overlays ()
|
|
"Deletes markers and overlays. Overlay arrow remains to indicate the last debug position."
|
|
(delete-overlay ess--dbg-current-debug-overlay)
|
|
(set-marker ess--dbg-current-debug-position nil))
|
|
|
|
|
|
;;;_ + Work Flow
|
|
(defun ess-debug-goto-input-event-marker ()
|
|
"Jump to the point where the last debugger/traceback etc event occurred.
|
|
|
|
Mainly useful during/after debugging, to jump to the place
|
|
from where the code was initially executed. This is an
|
|
electric-command, which means that after the command is triggered a
|
|
single key event is enough to navigate through the input-event-S-ring.
|
|
If the key-event which triggered the command is Shift modified
|
|
the input-event-S-ring is traversed backwards.
|
|
|
|
The input-event-S-ring is a virtual object which consists of two
|
|
rings `ess--dbg-forward-ring' and `ess--dbg-backward-ring' which
|
|
are joint at their tops.
|
|
|
|
See the more info at https://code.google.com/p/ess-tracebug/#Work-Flow"
|
|
(interactive)
|
|
(let* ((ev last-command-event)
|
|
(com-char (event-basic-type ev))
|
|
(ring-el 0)
|
|
input-point)
|
|
(if (memq 'shift (event-modifiers ev))
|
|
(setq input-point (ring-ref ess--dbg-backward-ring 0))
|
|
(ring-insert ess--dbg-backward-ring (point-marker)) ;; insert in backward ring ;;TODO: check if the marker to this (close by?) position is already in the ring
|
|
(setq input-point (ring-ref ess--dbg-forward-ring 0)))
|
|
(when (marker-buffer input-point) ;; TODO: give a message here if buff is not found
|
|
(pop-to-buffer-same-window (marker-buffer input-point))
|
|
(when (marker-position input-point)
|
|
(goto-char (marker-position input-point))))
|
|
(while (eq (event-basic-type (event-basic-type (setq ev (read-event)))) com-char)
|
|
(if (memq 'shift (event-modifiers ev))
|
|
(setq ring-el (1- ring-el))
|
|
(setq ring-el (1+ ring-el)))
|
|
(if (< ring-el 0)
|
|
(setq input-point (ring-ref ess--dbg-backward-ring (- ring-el))) ;; get it from backward-ring
|
|
;; get it from forward-ring
|
|
(setq input-point (ring-ref ess--dbg-forward-ring ring-el)) )
|
|
(when (marker-buffer input-point)
|
|
(pop-to-buffer-same-window (marker-buffer input-point))
|
|
(when (marker-position input-point)
|
|
(goto-char (marker-position input-point)))))
|
|
(push ev unread-command-events)))
|
|
|
|
(defun ess-debug-goto-debug-point ()
|
|
"Return to the debugging position.
|
|
Jump to markers stored in `ess--dbg-backward-ring'. If debug
|
|
session is active, first jump to current debug line.
|
|
|
|
This is an electric-command. Shift triggers the opposite traverse
|
|
of the ring."
|
|
(interactive)
|
|
(let* ((debug-point (ring-ref ess--dbg-backward-ring 0))
|
|
(ev last-command-event)
|
|
(com-char (event-basic-type ev))
|
|
(ring-el 0))
|
|
(if (ess--dbg-is-active-p)
|
|
(progn
|
|
(pop-to-buffer-same-window (marker-buffer ess--dbg-current-debug-position))
|
|
(goto-char (marker-position ess--dbg-current-debug-position ))
|
|
(back-to-indentation))
|
|
(pop-to-buffer-same-window (marker-buffer debug-point))
|
|
(goto-char (marker-position debug-point)))
|
|
(while (eq (event-basic-type (setq ev (read-event))) com-char)
|
|
(if (memq 'shift (event-modifiers ev))
|
|
(setq ring-el (1- ring-el))
|
|
(setq ring-el (1+ ring-el)))
|
|
(setq debug-point (ring-ref ess--dbg-backward-ring ring-el))
|
|
(when (marker-buffer debug-point)
|
|
(pop-to-buffer-same-window (marker-buffer debug-point))
|
|
(when (marker-position debug-point)
|
|
(goto-char (marker-position debug-point)))))
|
|
(push ev unread-command-events)))
|
|
|
|
(defun ess-debug-insert-in-forward-ring ()
|
|
"Insert `point-marker' into the forward-ring."
|
|
(interactive)
|
|
(ring-insert ess--dbg-forward-ring (point-marker))
|
|
(message "Point inserted into the forward-ring"))
|
|
|
|
(defvar ess-debug-indicator " DB"
|
|
"String to be displayed in mode-line alongside the process name.
|
|
Indicates that ess-debug-mode is turned on. When the debugger is
|
|
in active state this string is shown in upper case and
|
|
highlighted.")
|
|
|
|
(defvar-local ess--dbg-mode-line-debug
|
|
'(:eval (let ((proc (get-process ess-local-process-name)))
|
|
(if (and proc (process-get proc 'dbg-active))
|
|
(let ((str ess-debug-indicator))
|
|
(ess-debug-minor-mode 1) ; activate the keymap
|
|
(put-text-property 1 (length str)
|
|
'face '(:foreground "white" :background "red")
|
|
str)
|
|
str)
|
|
(ess-debug-minor-mode -1)
|
|
""))))
|
|
(put 'ess--dbg-mode-line-debug 'risky-local-variable t)
|
|
|
|
(defvar-local ess--dbg-mode-line-error-action
|
|
'(:eval (or (and (ess-process-live-p)
|
|
(ess-process-get 'on-error-action))
|
|
"")))
|
|
(put 'ess--dbg-mode-line-error-action 'risky-local-variable t)
|
|
|
|
(defun ess--dbg-remove-empty-lines (string)
|
|
"Remove empty lines from STRING (which interfere with evals) during debug.
|
|
This function is placed in `ess-presend-filter-functions'."
|
|
(if (and ess--dbg-del-empty-p (ess-process-get 'dbg-active))
|
|
(replace-regexp-in-string "\n\\s *$" "" string)
|
|
string))
|
|
|
|
|
|
(defun ess-debug-start ()
|
|
"Start the debug session.
|
|
Add to ESS the interactive debugging functionality, breakpoints,
|
|
watch and loggers. Integrates into ESS and iESS modes by binding
|
|
`ess-tracebug-map' to `ess-tracebug-prefix' in
|
|
`ess-mode-map' and `inferior-ess-mode-map' respectively."
|
|
(interactive)
|
|
(let ((dbuff (get-buffer-create (concat ess--dbg-output-buf-prefix "." ess-current-process-name "*"))) ;TODO: make dbuff a string!
|
|
(proc (ess-get-process ess-local-process-name))
|
|
(lpn ess-local-process-name))
|
|
(process-put proc 'dbg-buffer dbuff); buffer were the look up takes place
|
|
(process-put proc 'dbg-active nil) ; t if the process is in active debug state.
|
|
; Active debug states are usually those, in which prompt start with Browser[d]>
|
|
(set-process-filter proc 'inferior-ess-tracebug-output-filter)
|
|
(with-current-buffer (process-buffer proc)
|
|
(unless (equal ess-dialect "R")
|
|
(error "Can not activate the debugger for %s dialect" ess-dialect))
|
|
(add-to-list 'ess--mode-line-process-indicator 'ess--dbg-mode-line-debug t)
|
|
(add-to-list 'ess--mode-line-process-indicator 'ess--dbg-mode-line-error-action t)
|
|
|
|
(add-hook 'ess-presend-filter-functions 'ess--dbg-remove-empty-lines nil 'local))
|
|
(with-current-buffer dbuff
|
|
(setq ess-local-process-name lpn)
|
|
(buffer-disable-undo)
|
|
;; (setq buffer-read-only nil)
|
|
(make-local-variable 'overlay-arrow-position) ;; indicator for next-error functionality in the *ess.dbg*, useful??
|
|
(goto-char (point-max))
|
|
(setq ess--dbg-buf-p t ;; true if in *ess.dbg* buffer
|
|
ess--dbg-current-ref (point-marker) ;; used by goto-error functionality
|
|
ess--dbg-last-ref-marker (point-marker) ;; gives marker to reference of the last debugged line
|
|
)
|
|
;; (beginning-of-line)
|
|
;; (setq buffer-read-only t)
|
|
)))
|
|
|
|
(defun ess-debug-stop ()
|
|
"End the debug session.
|
|
Kill the *ess.dbg.[R_name]* buffer."
|
|
;;; process plist is not removed, TODO?low priority
|
|
(interactive)
|
|
(let ((proc (get-process ess-current-process-name))) ;;local?
|
|
(with-current-buffer (process-buffer proc)
|
|
(if (member ess-dialect '("XLS" "SAS" "STA"))
|
|
(error "Can not deactivate the debugger for %s dialect" ess-dialect))
|
|
(delq 'ess--dbg-mode-line-debug ess--mode-line-process-indicator)
|
|
(delq 'ess--dbg-mode-line-error-action ess--mode-line-process-indicator)
|
|
(remove-hook 'ess-presend-filter-functions 'ess--dbg-remove-empty-lines 'local))
|
|
(set-process-filter proc 'inferior-ess-output-filter)
|
|
(kill-buffer (process-get proc 'dbg-buffer))
|
|
(process-put proc 'dbg-buffer nil)
|
|
(process-put proc 'dbg-active nil)
|
|
;; (when (buffer-live-p ess--dbg-buffer)
|
|
;; ;; (with-current-buffer ess--dbg-buffer
|
|
;; ;; (set-buffer-modified-p nil)
|
|
;; ;; )
|
|
;; (kill-buffer ess--dbg-buffer)
|
|
;; )
|
|
))
|
|
|
|
|
|
(defun ess--make-busy-timer-function (process)
|
|
"Display the spinner of prompt if PROCESS is busy."
|
|
`(lambda ()
|
|
(let ((pb ,process))
|
|
(when (eq (process-status pb) 'run) ;; only when the process is alive
|
|
(with-current-buffer (process-buffer pb)
|
|
(if (not (process-get pb 'busy)) ;; if ready
|
|
(when (> ess--busy-count 0)
|
|
(setq ess--busy-count 0)
|
|
(force-mode-line-update)
|
|
(redisplay))
|
|
(setq ess--busy-count (1+ (mod ess--busy-count (1- (length ess-busy-strings)))))
|
|
(force-mode-line-update)
|
|
(redisplay)))))))
|
|
|
|
;; (ess--make-busy-prompt-function (get-process "R"))
|
|
|
|
(defun ess--dbg-is-active-p ()
|
|
"Return t if the current R process is in active debugging state."
|
|
(and (ess-process-live-p)
|
|
(ess-process-get 'dbg-active)))
|
|
|
|
(defun ess--dbg-is-recover-p ()
|
|
"Return t if the current R process is in active debugging state."
|
|
(and (ess-process-live-p)
|
|
(ess-process-get 'is-recover)))
|
|
|
|
(defun ess-debug-active-p (&optional proc)
|
|
(and (ess-process-live-p proc)
|
|
(or (ess-process-get 'dbg-active proc)
|
|
(ess-process-get 'is-recover proc))))
|
|
|
|
(defvar ess--dbg-regexp-reference "debug \\w+ +\\(.+\\)#\\([0-9]+\\):")
|
|
(defvar ess--dbg-regexp-jump "debug \\w+ ") ;; debug at ,debug bei ,etc
|
|
(defvar ess--dbg-regexp-skip
|
|
;; don't anchor to bol; secondary prompt can occur before (anything else?)
|
|
;; "\\(\\(?:Called from: \\)\\|\\(?:debugging in: \\)\\|\\(?:#[0-9]*: +recover()\\)\\)")
|
|
"\\(\\(?:Called from: \\)\\|\\(?:#[0-9]*: +recover()\\)\\)")
|
|
|
|
(defvar ess--dbg-regexp-no-skip
|
|
;; exceptions for first skip (magrittr)
|
|
"debug_pipe")
|
|
|
|
(defvar ess--dbg-regexp-debug "\\(\\(?:Browse[][0-9]+\\)\\|\\(?:debug: \\)\\)")
|
|
(defvar ess--dbg-regexp-selection "\\(Selection: \\'\\)")
|
|
(defvar ess--dbg-regexp-input (concat ess--dbg-regexp-debug "\\|"
|
|
ess--dbg-regexp-selection))
|
|
|
|
(defvar ess--suppress-next-output? nil)
|
|
|
|
|
|
|
|
;;; MPI
|
|
|
|
;; http://jkorpela.fi/chars/c0.html
|
|
;; https://en.wikipedia.org/wiki/ANSI_escape_code#Escape_sequences
|
|
(defvar ess-mpi-message-start-delimiter "_")
|
|
(defvar ess-mpi-message-field-separator "")
|
|
(defvar ess-mpi-message-end-delimiter "\\")
|
|
|
|
(define-obsolete-variable-alias 'ess-mpi-alist 'ess-mpi-handlers "ESS 19.04")
|
|
(defvar ess-mpi-handlers
|
|
'(("message" . message)
|
|
("error" . ess-mpi:error)
|
|
("eval" . ess-mpi:eval)
|
|
("y-or-n" . ess-mpi:y-or-n))
|
|
"Alist of the MPI handlers.
|
|
Each element is of the form (TYPE . HANDLER), where TYPE is the
|
|
message type and HANDLER is a function (symbol) to be called on
|
|
the payload list of each message.")
|
|
|
|
(defun ess-mpi:error (msg)
|
|
(error "MPI error: %s" msg))
|
|
|
|
(defun ess-mpi:eval (str &optional callback)
|
|
"Read STR and evaluate as Emacs expression.
|
|
If present, the CALLBACK string is passed through `format' with
|
|
returned value from EXPR and then sent to the subprocess."
|
|
(let ((result (eval (read str))))
|
|
(when callback
|
|
(ess-send-string (ess-get-process) (format callback result)))))
|
|
|
|
(defun ess-mpi:y-or-n (prompt callback)
|
|
"Ask `y-or-n-p' with PROMPT.
|
|
The CALLBACK string is passed through `format' with returned
|
|
value from EXPR and then sent to the subprocess."
|
|
(let ((result (y-or-n-p prompt)))
|
|
(when callback
|
|
(let ((result (if result "TRUE" "FALSE")))
|
|
(ess-send-string (ess-get-process) (format callback result))))))
|
|
|
|
(defun ess-mpi-convert (el)
|
|
(cond
|
|
((string= el "nil") nil)
|
|
((string= el "t") t)
|
|
(t el)))
|
|
|
|
(defun ess-mpi-handle-messages (buf)
|
|
"Handle all mpi messages in BUF and delete them.
|
|
The MPI message has the form TYPEFIELD... where TYPE is the
|
|
type of the messages on which handlers in `ess-mpi-handlers' are
|
|
dispatched. And FIELDs are strings. Return :incomplete if BUF
|
|
ends with an incomplete message."
|
|
(let ((obuf (current-buffer))
|
|
(out nil))
|
|
(with-current-buffer buf
|
|
(goto-char (point-min))
|
|
;; This should be smarter because Emacs might cut it in the middle of the
|
|
;; message. In practice this almost never happen because we are
|
|
;; accumulating output into the cache buffer.
|
|
(while (search-forward ess-mpi-message-start-delimiter nil t)
|
|
(let ((mbeg0 (match-beginning 0))
|
|
(mbeg (match-end 0)))
|
|
(if (search-forward ess-mpi-message-end-delimiter nil t)
|
|
(let* ((mend (match-beginning 0))
|
|
(mend0 (match-end 0))
|
|
(msg (buffer-substring mbeg mend))
|
|
(payload (mapcar #'ess-mpi-convert
|
|
(split-string msg ess-mpi-message-field-separator)))
|
|
(head (pop payload))
|
|
(handler (cdr (assoc head ess-mpi-handlers))))
|
|
(unwind-protect
|
|
(if handler
|
|
(with-current-buffer obuf
|
|
(apply handler payload))
|
|
(error "No handler defined for MPI message '%s" head))
|
|
(goto-char mbeg0)
|
|
(delete-region mbeg0 mend0)))
|
|
(setq out :incomplete))))
|
|
out)))
|
|
|
|
(defun ess--replace-long+-in-prompt (prompt is-final)
|
|
"Replace long + + + in PROMPT based on `inferior-ess-replace-long+' value.
|
|
If IS-FINAL means that PROMPT occurs at the end of the process
|
|
chunk. If non-nil, special care is taken not to drop last '+'
|
|
value as it might be a continuation prompt."
|
|
;; see #576 for interesting input examples
|
|
(let ((len (length prompt)))
|
|
(if (or (null inferior-ess-replace-long+)
|
|
(< len 2))
|
|
prompt
|
|
(let ((last+ (eq (elt prompt (- len 2)) ?+)))
|
|
(cond
|
|
((eq inferior-ess-replace-long+ 'strip)
|
|
(if (and last+ is-final)
|
|
"+ "
|
|
"> "))
|
|
((eq inferior-ess-replace-long+ t)
|
|
(let ((prompt (replace-regexp-in-string "\\(\\+ \\)\\{2\\}\\(\\+ \\)+"
|
|
ess-long+replacement prompt)))
|
|
(if (and last+ (not is-final))
|
|
;; append > for aesthetic reasons
|
|
(concat prompt "> ")
|
|
prompt)))
|
|
(t (error "Invalid values of `inferior-ess-replace-long+'")))))))
|
|
|
|
(defun ess--offset-output (prev-prompt str)
|
|
"Add suitable offset to STR given the preceding PREV-PROMPT."
|
|
(if prev-prompt
|
|
(let ((len (length prev-prompt)))
|
|
;; prompts have at least 2 chars
|
|
(if (eq (elt prev-prompt (- len 2)) ?+)
|
|
;; when last + append > for aesthetic reasons
|
|
(concat "> \n" str)
|
|
(if (eq (elt str 0) ?\n)
|
|
;; don't insert empty lines
|
|
str
|
|
(concat "\n" str))))
|
|
str))
|
|
|
|
(defun ess--flush-accumulated-output (proc)
|
|
"Flush accumulated output of PROC into its output buffer.
|
|
Insertion happens chunk by chunk. A chunk is a region between two
|
|
prompts."
|
|
(let* ((abuf (ess--accumulation-buffer proc))
|
|
(pbuf (process-buffer proc))
|
|
(visibly (process-get proc :eval-visibly))
|
|
(nowait (eq visibly 'nowait))
|
|
(flush-timer (process-get proc 'flush-timer)))
|
|
(when (> (buffer-size abuf) 0)
|
|
(when (timerp flush-timer)
|
|
(cancel-timer flush-timer))
|
|
(if (eq (buffer-local-value 'major-mode pbuf) 'fundamental-mode)
|
|
;; FIXME: this cannot be, ess-command changes the filter
|
|
;; Just in case if we are in *ess-command* buffer; restart the timer.
|
|
(process-put proc 'flush-timer
|
|
(run-at-time .02 nil #'ess--flush-accumulated-output proc))
|
|
;; Incomplete mpi should hardly happen. Only on those rare occasions
|
|
;; when an mpi is issued after a long task and split by the Emacs input
|
|
;; handler, or mpi printing itself takes very long.
|
|
(unless (eq :incomplete (ess-mpi-handle-messages abuf))
|
|
(with-current-buffer abuf
|
|
(goto-char (point-min))
|
|
(let ((case-fold-search nil))
|
|
(when (re-search-forward "Error\\(:\\| +in\\)" nil t)
|
|
(unless (get-buffer-window pbuf 'visible)
|
|
(display-buffer (process-buffer proc) nil t))))
|
|
(goto-char (point-min))
|
|
;; First long + + in the output mirrors the sent input by the user and
|
|
;; is unnecessary in nowait case. A single + can be a continuation in
|
|
;; the REPL, thus we check if there is an extra output after the + .
|
|
(when nowait
|
|
(when (looking-at "\\([+>] \\)\\{2,\\}\n?")
|
|
(goto-char (match-end 0))
|
|
(when (eq (point) (point-max))
|
|
;; if this is the last prompt in the output back-up one prompt
|
|
;; (cannot happen after \n)
|
|
(backward-char 2))))
|
|
(let ((do-clean (not (eq visibly t)))
|
|
(pos2 (point))
|
|
(pos1 (point))
|
|
(tpos nil)
|
|
(prompt nil)
|
|
(regexp (if nowait
|
|
;; we cannot disambiguate printed input fields and
|
|
;; prompts in output in this case; match 2+ pluses or
|
|
;; > and 2+ spaces
|
|
"\\(^\\([+>] \\)\\{2,\\}\\)\\|\\(> \\) +"
|
|
"^\\([+>] \\)+"))
|
|
(prev-prompt (process-get proc 'prev-prompt)))
|
|
(while (re-search-forward regexp nil t)
|
|
(setq pos1 (match-beginning 0)
|
|
tpos (if nowait
|
|
(or (match-end 1) (match-end 3))
|
|
(match-end 0)))
|
|
;; for debugging in R:accum window in order to see the pointer moving
|
|
;; (set-window-point (get-buffer-window) tpos)
|
|
(when (> pos1 pos2)
|
|
(let ((str (buffer-substring pos2 pos1)))
|
|
(comint-output-filter proc (ess--offset-output prev-prompt str))))
|
|
(setq pos2 tpos)
|
|
(setq prompt (let ((prompt (buffer-substring pos1 pos2)))
|
|
(if do-clean
|
|
(ess--replace-long+-in-prompt prompt (eq pos2 (point-max)))
|
|
prompt)))
|
|
;; Cannot bypass this trivial call to comint-output-filter because
|
|
;; external tools could rely on prompts (org-babel [#598] for
|
|
;; example). Setting dummy regexp in order to avoid comint erasing
|
|
;; this prompt which contrasts to how we output prompts in all
|
|
;; other cases.
|
|
(with-current-buffer pbuf
|
|
(let ((comint-prompt-regexp "^$"))
|
|
(comint-output-filter proc prompt)))
|
|
(setq prev-prompt (and do-clean prompt)
|
|
pos1 pos2))
|
|
;; insert last chunk if any
|
|
(unless (eq pos1 (point-max))
|
|
(let ((str (buffer-substring-no-properties pos1 (point-max))))
|
|
(comint-output-filter proc (ess--offset-output prev-prompt str))
|
|
(setq prev-prompt nil)))
|
|
(process-put proc 'prev-prompt prev-prompt)
|
|
(process-put proc 'flush-time (and (process-get proc 'busy)
|
|
(float-time)))
|
|
(erase-buffer))))))))
|
|
|
|
(defun inferior-ess-tracebug-output-filter (proc string)
|
|
"Standard output filter for the inferior ESS process.
|
|
When `ess-debug' is active, this is the filter. Call
|
|
`inferior-ess-output-filter'. Check for debug
|
|
reg-expressions (see `ess--dbg-regexp-debug',...), when found
|
|
puts iESS in the debugging state. If in debugging state, mirrors
|
|
the output into *ess.dbg* buffer."
|
|
(let* ((is-iess (or (derived-mode-p 'ess-watch-mode)
|
|
(derived-mode-p 'inferior-ess-mode)))
|
|
(pbuf (process-buffer proc))
|
|
(abuf (ess--accumulation-buffer proc))
|
|
(dbuff (process-get proc 'dbg-buffer))
|
|
(wbuff (get-buffer ess-watch-buffer))
|
|
(was-in-dbg (process-get proc 'dbg-active))
|
|
(was-in-recover (process-get proc 'is-recover))
|
|
(input-point (point-marker))
|
|
(match-jump (string-match ess--dbg-regexp-jump string))
|
|
(match-input (string-match ess--dbg-regexp-input string))
|
|
(match-selection (and match-input
|
|
(match-string 2 string))) ;; Selection:
|
|
(match-skip (and ess-debug-skip-first-call
|
|
(string-match ess--dbg-regexp-skip string)
|
|
(not (string-match ess--dbg-regexp-no-skip string))))
|
|
(match-dbg (or match-skip (and match-input (not match-selection))))
|
|
(is-ready (inferior-ess--set-status proc string))
|
|
(new-time (float-time))
|
|
(last-time (process-get proc 'flush-time))
|
|
(flush-timer (process-get proc 'flush-timer)))
|
|
|
|
;; current-buffer is still the user's input buffer here
|
|
(ess--if-verbose-write-process-state proc string)
|
|
(inferior-ess-run-callback proc string)
|
|
(process-put proc 'is-recover match-selection)
|
|
|
|
(if (or (process-get proc 'suppress-next-output?)
|
|
ess--suppress-next-output?)
|
|
|
|
;; works only for suppressing short output, enough for now (for callbacks)
|
|
(process-put proc 'suppress-next-output? nil)
|
|
|
|
(with-current-buffer abuf
|
|
(goto-char (point-max))
|
|
(insert string))
|
|
|
|
;; cancel the timer each time we enter this filter
|
|
(when (timerp flush-timer)
|
|
(cancel-timer flush-timer)
|
|
(process-put proc 'flush-timer nil))
|
|
|
|
(unless last-time ;; don't flush for the first time
|
|
(setq last-time new-time)
|
|
(process-put proc 'flush-time new-time))
|
|
|
|
;; flush periodically
|
|
(let ((fast-flush (or is-ready
|
|
;; for the sake of ess-eval-linewise
|
|
(process-get proc 'sec-prompt))))
|
|
(if (or
|
|
;; theoretically we should flush asynchronously in all cases but
|
|
;; somewhat unexpectedly it introduces much more randomness during
|
|
;; batch testing. TODO: flush directly for now and either remove or
|
|
;; improve on the next refactoring iteration
|
|
fast-flush
|
|
(> (- new-time last-time) .5)
|
|
(bound-and-true-p edebug-mode)
|
|
;; the flush is not getting called if the third party call
|
|
;; accept-process-output in a loop (e.g. org-babel-execute-src-block)
|
|
(bound-and-true-p org-babel-current-src-block-location))
|
|
(ess--flush-accumulated-output proc)
|
|
;; Setup new flush timer. Ideally also for fast-flush case in order to
|
|
;; avoid detecting intermediate prompts as end-of-output prompts.
|
|
(let ((timeout (if fast-flush .01 .2)))
|
|
(process-put proc 'flush-timer
|
|
(run-at-time timeout nil #'ess--flush-accumulated-output proc))))))
|
|
|
|
;; WATCH
|
|
(when (and is-ready wbuff) ;; refresh only if the process is ready and wbuff exists, (not only in the debugger!!)
|
|
(ess-watch-refresh-buffer-visibly wbuff))
|
|
|
|
;; JUMP to line if debug expression was matched
|
|
(when match-jump
|
|
(with-current-buffer dbuff ;; insert string in *ess.dbg* buffer
|
|
(goto-char (point-max))
|
|
(insert (concat "|-" string "-|")))
|
|
(ess--dbg-goto-last-ref-and-mark dbuff is-iess))
|
|
|
|
;; (with-current-buffer dbuff ;; un-comment to see the value of STRING just before debugger exists
|
|
;; (let ((inhibit-read-only t))
|
|
;; (goto-char (point-max))
|
|
;; (insert (concat " ---\n " string "\n ---"))
|
|
;; ))
|
|
|
|
;; SKIP if needed
|
|
(when (and match-skip (not was-in-recover))
|
|
(process-send-string proc "n\n"))
|
|
|
|
;; EXIT the debugger
|
|
(when (and was-in-dbg
|
|
(not (or match-jump match-dbg))
|
|
(or is-ready match-selection))
|
|
(ess--dbg-deactivate-overlays)
|
|
(process-put proc 'dbg-active nil)
|
|
;; (message "|<-- exited debugging -->|")
|
|
(when wbuff
|
|
(ess-watch-refresh-buffer-visibly wbuff)))
|
|
|
|
;; ACTIVATE the debugger if entered for the first time
|
|
(when (and (not was-in-dbg)
|
|
(not match-selection)
|
|
(or match-jump match-dbg))
|
|
(unless is-iess
|
|
(ring-insert ess--dbg-forward-ring input-point))
|
|
(process-put proc 'dbg-active t)
|
|
(message
|
|
(ess--debug-keys-message-string))
|
|
(unless match-jump
|
|
;; no source reference, simply show the inferior
|
|
(display-buffer pbuf)))
|
|
|
|
(when match-selection ;(and (not was-in-recover) match-selection)
|
|
(ess-electric-selection t))))
|
|
|
|
|
|
(defvar ess-debug-minor-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(define-key map (kbd "M-C") #'ess-debug-command-continue)
|
|
(define-key map [(control meta ?C)] #'ess-debug-command-continue-multi)
|
|
(define-key map (kbd "M-N") #'ess-debug-command-next)
|
|
(define-key map [(control meta ?N)] #'ess-debug-command-next-multi)
|
|
(define-key map (kbd "M-Q") #'ess-debug-command-quit)
|
|
(define-key map (kbd "M-U") #'ess-debug-command-up)
|
|
map)
|
|
"Keymap active when ESS process is in debugging state.
|
|
\\{ess-debug-minor-mode-map}")
|
|
|
|
|
|
(define-minor-mode ess-debug-minor-mode
|
|
"Minor mode activated when ESS process is in debugging state."
|
|
:lighter nil
|
|
:keymap ess-debug-minor-mode-map)
|
|
|
|
(defun ess--dbg-goto-last-ref-and-mark (dbuff &optional other-window)
|
|
"Open the most recent debug reference, and set all the necessary marks and overlays.
|
|
It's called from `inferior-ess-tracebug-output-filter'. DBUFF
|
|
must be the *ess.dbg* buffer associated with the process. If
|
|
OTHER-WINDOW is non nil, attempt to open the location in a
|
|
different window."
|
|
(let (t-debug-position ref)
|
|
(with-current-buffer dbuff
|
|
(setq ref (ess--dbg-get-next-ref -1 (point-max) ess--dbg-last-ref-marker
|
|
ess--dbg-regexp-reference)) ; sets point at the end of found ref
|
|
(when ref
|
|
(move-marker ess--dbg-last-ref-marker (point-at-eol))
|
|
;; each new step repositions the current-ref!
|
|
(move-marker ess--dbg-current-ref ess--dbg-last-ref-marker)))
|
|
(when ref
|
|
(let ((buf (apply 'ess--dbg-goto-ref other-window ref)))
|
|
(if buf
|
|
;; if referenced buffer has been found, put overlays:
|
|
(with-current-buffer buf
|
|
(setq t-debug-position (copy-marker (point-at-bol)))
|
|
(if (equal t-debug-position ess--dbg-current-debug-position)
|
|
(progn ;; highlights the overlay for ess--dbg-blink-interval seconds
|
|
(overlay-put ess--dbg-current-debug-overlay 'face 'ess--dbg-blink-same-ref-face)
|
|
(run-with-timer ess-debug-blink-interval nil
|
|
(lambda ()
|
|
(overlay-put ess--dbg-current-debug-overlay 'face 'ess-debug-current-debug-line-face))))
|
|
;; else
|
|
(ess--dbg-activate-overlays)))
|
|
;;else, buffer is not found: highlight and give the corresponding message
|
|
(overlay-put ess--dbg-current-debug-overlay 'face 'ess--dbg-blink-ref-not-found-face)
|
|
(run-with-timer ess-debug-blink-interval nil
|
|
(lambda ()
|
|
(overlay-put ess--dbg-current-debug-overlay 'face 'ess-debug-current-debug-line-face)))
|
|
(message "Reference %s not found" (car ref)))))))
|
|
|
|
(defun ess--dbg-goto-ref (other-window file line &optional col)
|
|
"Opens the reference given by FILE, LINE and COL.
|
|
Try to open in a different window if OTHER-WINDOW is nil. Return
|
|
the buffer if found, or nil otherwise be found.
|
|
`ess--dbg-find-buffer' is used to find the FILE and open the
|
|
associated buffer. If FILE is nil return nil."
|
|
(let ((mrk (car (ess--dbg-create-ref-marker file line col)))
|
|
(lpn ess-local-process-name))
|
|
(when mrk
|
|
(let ((buf (marker-buffer mrk)))
|
|
(if (not other-window)
|
|
(pop-to-buffer-same-window buf)
|
|
(let ((this-frame (window-frame (get-buffer-window (current-buffer)))))
|
|
(display-buffer buf)
|
|
;; simple save-frame-excursion
|
|
(unless (eq this-frame (window-frame (get-buffer-window buf t)))
|
|
(ess-select-frame-set-input-focus this-frame))))
|
|
;; set or re-set to lpn as this is the process with debug session on
|
|
(with-current-buffer buf
|
|
(setq ess-local-process-name lpn)
|
|
(goto-char mrk)
|
|
(set-window-point (get-buffer-window buf) mrk))
|
|
buf))))
|
|
|
|
;; temporary, hopefully org folks implement something similar
|
|
(defvar org-babel-tangled-file nil)
|
|
(declare-function org-babel-tangle-jump-to-org "ob-tangle.el")
|
|
|
|
(defun ess--dbg-create-ref-marker (file line &optional col)
|
|
"Create markers to the reference given by FILE, LINE and COL.
|
|
Return list of two markers MK-start and MK-end. MK-start is the
|
|
position of error. Mk-end is the end of the line where error
|
|
occurred. If buffer associated with FILE is not found, or line is
|
|
nil, or TB-INDEX is not found return nil."
|
|
(if (stringp line) (setq line (string-to-number line)))
|
|
(if (stringp col) (setq col (string-to-number col)))
|
|
(let* ((srcref (gethash file ess--srcrefs))
|
|
(file (replace-regexp-in-string "^\n" "" ;; hack for gnu regexp
|
|
(or (car srcref) file)))
|
|
(tb-index (cadr srcref))
|
|
(buffer (ess--dbg-find-buffer file))
|
|
pos)
|
|
(when (and buffer line)
|
|
(save-excursion
|
|
(with-current-buffer buffer
|
|
(save-restriction
|
|
(widen) ;; how does this behave in narrowed buffers? tothink:
|
|
(goto-char 1)
|
|
(setq pos (point))
|
|
(when tb-index
|
|
(while (and (not (eq tb-index (get-text-property pos 'tb-index)))
|
|
(setq pos (next-single-property-change pos 'tb-index)))))
|
|
(unless pos
|
|
;; use beg position if index not found
|
|
(setq pos (nth 2 srcref)))
|
|
(when pos
|
|
(goto-char pos)
|
|
(forward-line (1- line))
|
|
(if col
|
|
(goto-char (+ (point-at-bol) col))
|
|
(back-to-indentation))
|
|
(when (bound-and-true-p org-babel-tangled-file)
|
|
(org-babel-tangle-jump-to-org))
|
|
(list (point-marker) (copy-marker (point-at-eol))))))))))
|
|
|
|
(defun ess--dbg-find-buffer (filename)
|
|
"Find a buffer for file FILENAME.
|
|
If FILENAME is not found at all, ask the user where to find it if
|
|
`ess--dbg-ask-for-file' is non-nil. Search the directories in
|
|
`ess-tracebug-search-path'."
|
|
(let ((dirs (append
|
|
(ess-r-package-source-dirs)
|
|
(cl-loop for d in ess-tracebug-search-path
|
|
append (ess-r-package--all-source-dirs d))))
|
|
buffer name)
|
|
(setq dirs (cons default-directory dirs)) ;; TODO: should be R working dir
|
|
;; 1. search already open buffers for match (associated file might not even exist yet)
|
|
(cl-dolist (bf (buffer-list))
|
|
(with-current-buffer bf
|
|
(when (and buffer-file-name
|
|
(or (and (file-name-absolute-p filename)
|
|
(string-match (format "%s\\'" filename) buffer-file-name))
|
|
(equal filename (file-name-nondirectory buffer-file-name))))
|
|
(setq buffer bf)
|
|
(cl-return))))
|
|
;; 2. The file name is absolute. Use its explicit directory as
|
|
;; the first in the search path, and strip it from FILENAME.
|
|
(when (and (null buffer)
|
|
(file-name-absolute-p filename))
|
|
(setq filename (abbreviate-file-name (expand-file-name filename))
|
|
dirs (cons (file-name-directory filename) dirs)
|
|
filename (file-name-nondirectory filename)))
|
|
;; 3. Now search the path.
|
|
(while (and (null buffer) dirs)
|
|
(let ((thisdir (pop dirs)))
|
|
(setq name (expand-file-name filename thisdir)
|
|
buffer (and (file-exists-p name)
|
|
(find-file-noselect name)))))
|
|
;; 4. Ask for file if not found (tothink: maybe remove this part?)
|
|
(if (and (null buffer)
|
|
ess-debug-ask-for-file)
|
|
(save-excursion ;This save-excursion is probably not right.
|
|
(let* ((pop-up-windows t)
|
|
(name (read-file-name
|
|
(format "Find next line in (default %s): " filename)
|
|
nil filename t nil))
|
|
(origname name))
|
|
(cond
|
|
((not (file-exists-p name))
|
|
(message "Cannot find file `%s'" name)
|
|
(ding) (sit-for 2))
|
|
((and (file-directory-p name)
|
|
(not (file-exists-p
|
|
(setq name (expand-file-name filename name)))))
|
|
(message "No `%s' in directory %s" filename origname)
|
|
(ding) (sit-for 2))
|
|
(t
|
|
(setq buffer (find-file-noselect name)))))))
|
|
;; nil if not found
|
|
buffer))
|
|
|
|
(defun ess--dbg-get-next-ref (n &optional pt BOUND REG nF nL nC)
|
|
"Move point to the next reference in the *ess.dbg* buffer.
|
|
|
|
Must be called from *ess.dbg* buffer.
|
|
It returns the reference in the form (file line col) /all strings/ ,
|
|
or NIL if not found . Prefix arg N says how many error messages
|
|
to move forwards (or backwards, if negative). Optional arg PT,
|
|
if non-nil, specifies the value of point to start looking for the
|
|
next message, default to (point). BOUND is the limiting position
|
|
of the search. REG is the regular expression to search with. nF
|
|
- sub-expression of REG giving the 'file'; defaults to 1. nL -
|
|
giving the 'line'; defaults to 2. nC - sub-expr giving the
|
|
'column'; defaults to 3."
|
|
(unless ess--dbg-buf-p
|
|
(error "Not in *ess.dbg* buffer"))
|
|
(setq nF (or nF 1)
|
|
nL (or nL 2)
|
|
nC (or nC 3))
|
|
(or pt (setq pt (point)))
|
|
;; (message "ess--dbg-last-ref-marker%s vs pt%s vs point-max%s" ess--dbg-last-ref-marker pt (point-max))
|
|
(goto-char pt)
|
|
(if (search-forward-regexp REG BOUND t n)
|
|
(list (match-string nF) (match-string-no-properties nL) (match-string-no-properties nC))
|
|
nil))
|
|
|
|
(defun ess--debug-keys-message-string (&optional map)
|
|
(let ((overriding-local-map (or map ess-debug-minor-mode-map)))
|
|
(substitute-command-keys
|
|
(mapconcat 'identity
|
|
'("(\\[ess-debug-command-continue])cont"
|
|
"(\\[ess-debug-command-continue-multi])cont-multi"
|
|
"(\\[ess-debug-command-next])next"
|
|
"(\\[ess-debug-command-next-multi])next-multi"
|
|
"(\\[ess-debug-command-up])up"
|
|
"(\\[ess-debug-command-quit])quit")
|
|
" "))))
|
|
|
|
(defun ess-electric-selection (&optional wait)
|
|
"Call commands defined in `ess-electric-selection-map'.
|
|
Single-key input commands are those, which once executed do not
|
|
require the prefix command for subsequent invocation.
|
|
|
|
If WAIT is t, wait for next input and ignore the keystroke which
|
|
triggered the command."
|
|
(interactive)
|
|
(ess--execute-electric-command ess-electric-selection-map
|
|
"Selection: " wait
|
|
(not (ess-process-get 'is-recover))))
|
|
|
|
(defun ess-debug-command-digit (&optional ev)
|
|
"Digit commands in selection mode.
|
|
If supplied, EV must be a proper key event or a string representing the digit."
|
|
(interactive)
|
|
(ess-force-buffer-current)
|
|
(unless (ess--dbg-is-recover-p)
|
|
(error "Recover is not active"))
|
|
(unless ev
|
|
(setq ev last-command-event))
|
|
(let* ((ev-char (if (stringp ev)
|
|
ev
|
|
(char-to-string (event-basic-type ev))))
|
|
(proc (get-process ess-current-process-name))
|
|
(mark-pos (marker-position (process-mark proc)))
|
|
(comint-prompt-read-only nil)
|
|
prompt depth)
|
|
(with-current-buffer (process-buffer proc)
|
|
(goto-char mark-pos)
|
|
(save-excursion
|
|
(when (re-search-backward "\\(?: \\|^\\)\\([0-9]+\\):[^\t]+Selection:" ess--tb-last-input t)
|
|
(setq depth (string-to-number (match-string 1)))
|
|
(when (> depth 9)
|
|
(setq ev-char (ess-completing-read "Selection" (mapcar 'number-to-string
|
|
(number-sequence depth 0 -1))
|
|
nil t ev-char nil)))))
|
|
(setq prompt (delete-and-extract-region (point-at-bol) mark-pos))
|
|
(insert (concat prompt ev-char "\n"))
|
|
(ess-send-string proc ev-char)
|
|
(move-marker (process-mark proc) (max-char)))))
|
|
|
|
(defun ess-debug-command-next ()
|
|
"Step next in debug mode.
|
|
Equivalent to 'n' at the R prompt."
|
|
(interactive)
|
|
(ess-force-buffer-current)
|
|
(unless (ess--dbg-is-active-p)
|
|
(error "Debugger is not active"))
|
|
(if (ess--dbg-is-recover-p)
|
|
(ess-send-string (ess-get-process) "0")
|
|
(ess-send-string (ess-get-process) "n")))
|
|
|
|
(defun ess-debug-command-next-multi (&optional N)
|
|
"Ask for N and step (n) N times in debug mode."
|
|
(interactive)
|
|
(ess-force-buffer-current)
|
|
(unless (ess--dbg-is-active-p)
|
|
(error "Debugger is not active"))
|
|
(let ((N (or N (read-number "Number of steps: " 10)))
|
|
(ess--suppress-next-output? t))
|
|
(while (and (ess--dbg-is-active-p) (> N 0))
|
|
(ess-debug-command-next)
|
|
(ess-wait-for-process)
|
|
(setq N (1- N))))
|
|
(ess-debug-command-next))
|
|
|
|
(defun ess-debug-command-continue-multi (&optional N)
|
|
"Ask for N, and continue (c) N times in debug mode."
|
|
(interactive)
|
|
(ess-force-buffer-current)
|
|
(unless (ess--dbg-is-active-p)
|
|
(error "Debugger is not active"))
|
|
(let ((N (or N (read-number "Number of continuations: " 10)))
|
|
(ess--suppress-next-output? t))
|
|
(while (and (ess--dbg-is-active-p) (> N 1))
|
|
(ess-debug-command-continue)
|
|
(ess-wait-for-process)
|
|
(setq N (1- N))))
|
|
(ess-debug-command-continue))
|
|
|
|
(defun ess-debug-command-up ()
|
|
"Step up one call frame.
|
|
Equivalent to 'n' at the R prompt."
|
|
(interactive)
|
|
(ess-force-buffer-current)
|
|
(unless (ess--dbg-is-active-p)
|
|
(error "Debugger is not active"))
|
|
(let ((up-cmd "try(browserSetDebug(), silent=T)\nc\n"))
|
|
(ess-send-string (ess-get-process) up-cmd)))
|
|
|
|
;; (defun ess-debug-previous-error (&optional ev)
|
|
;; "Go to previous reference during the debug process.
|
|
;; R doesn't support step backwards. This command just takes you through
|
|
;; debug history."
|
|
;; (interactive)
|
|
;; (previous-error))
|
|
|
|
(defun ess-debug-command-quit ()
|
|
"Quits the browser/debug in R process.
|
|
Equivalent of `Q' at the R prompt."
|
|
(interactive)
|
|
(ess-force-buffer-current)
|
|
(cond ((ess--dbg-is-recover-p)
|
|
(ess-send-string (ess-get-process) "0" t))
|
|
;; if recover is called in a loop the following stalls Emacs
|
|
;; (ess-wait-for-process proc nil 0.05)
|
|
((ess--dbg-is-active-p)
|
|
(ess-send-string (ess-get-process) "Q" t))
|
|
(t
|
|
(error "Debugger is not active"))))
|
|
|
|
(defun ess-debug-command-continue ()
|
|
"Continue the code execution.
|
|
Equivalent of `c' at the R prompt."
|
|
(interactive)
|
|
(ess-force-buffer-current)
|
|
(cond ((ess--dbg-is-recover-p)
|
|
(ess-send-string (ess-get-process) "0"))
|
|
((ess--dbg-is-active-p)
|
|
(ess-send-string (ess-get-process) "c"))
|
|
(t
|
|
(error "Debugger is not active"))))
|
|
|
|
(defun ess-tracebug-set-last-input (&rest _args)
|
|
"Move `ess--tb-last-input' marker to the process mark.
|
|
ARGS are ignored to allow using this function in process hooks."
|
|
(let* ((last-input-process (get-process ess-local-process-name))
|
|
(last-input-mark (copy-marker (process-mark last-input-process))))
|
|
(with-current-buffer (process-buffer last-input-process)
|
|
(when (local-variable-p 'ess--tb-last-input) ;; TB might not be active in all processes
|
|
(save-excursion
|
|
(setq ess--tb-last-input last-input-mark)
|
|
(goto-char last-input-mark)
|
|
(inferior-ess-move-last-input-overlay))))))
|
|
|
|
;;;_ + BREAKPOINTS
|
|
|
|
(defface ess-bp-fringe-inactive-face
|
|
'((((class color) (background light) (min-colors 88)) (:foreground "DimGray"))
|
|
(((class color) (background dark) (min-colors 88)) (:foreground "LightGray"))
|
|
(((background light) (min-colors 8)) (:foreground "blue"))
|
|
(((background dark) (min-colors 8)) (:foreground "cyan")))
|
|
"Face used to highlight inactive breakpoints."
|
|
:group 'ess-debug)
|
|
|
|
(defface ess-bp-fringe-logger-face
|
|
'((((class color) (background light) (min-colors 88)) (:foreground "dark red"))
|
|
(((class color) (background dark) (min-colors 88)) (:foreground "tomato1"))
|
|
(((background light) (min-colors 8)) (:foreground "blue"))
|
|
(((background dark) (min-colors 8)) (:foreground "cyan")))
|
|
"Face used to highlight loggers."
|
|
:group 'ess-debug)
|
|
|
|
(defface ess-bp-fringe-browser-face
|
|
'((((class color) (background light) (min-colors 88)) (:foreground "medium blue"))
|
|
(((class color) (background dark) (min-colors 88)) (:foreground "deep sky blue"))
|
|
(((background light) (min-colors 8)) (:foreground "blue"))
|
|
(((background dark) (min-colors 8)) (:foreground "cyan")))
|
|
"Face used to highlight 'browser' breakpoints."
|
|
:group 'ess-debug)
|
|
|
|
(defface ess-bp-fringe-recover-face
|
|
'((((class color) (background light) (min-colors 88)) (:foreground "dark magenta"))
|
|
(((class color) (background dark) (min-colors 88)) (:foreground "magenta"))
|
|
(((background light) (min-colors 8)) (:foreground "magenta"))
|
|
(((background dark) (min-colors 8)) (:foreground "magenta")))
|
|
"Face used to highlight 'recover' breakpoints fringe."
|
|
:group 'ess-debug)
|
|
|
|
(defun ess--bp-pipe-block-p ()
|
|
(save-excursion
|
|
(let ((inhibit-point-motion-hooks t)
|
|
(inhibit-field-text-motion t))
|
|
(forward-line -1)
|
|
(end-of-line)
|
|
(looking-back "%>%[ \t]*" (point-at-bol)))))
|
|
|
|
(defvar ess--bp-identifier 1)
|
|
(defcustom ess-bp-type-spec-alist
|
|
'((pipe ".ess_pipe_browser() %%>%%" "B %>%\n" filled-square ess-bp-fringe-browser-face ess--bp-pipe-block-p)
|
|
(browser "browser(expr=is.null(.ESSBP.[[%s]]));" "B>\n" filled-square ess-bp-fringe-browser-face)
|
|
(recover "recover()" "R>\n" filled-square ess-bp-fringe-recover-face))
|
|
"List of lists of breakpoint types.
|
|
Each sublist has five elements:
|
|
1- symbol giving the name of specification
|
|
2- R expression to be inserted (%s is substituted with unique identifier).
|
|
3- string to be displayed instead of the expression
|
|
4- fringe bitmap to use
|
|
5- face for fringe and displayed string
|
|
6- optional, a function which should return nil if this BP doesn't apply to current context."
|
|
:group 'ess-debug
|
|
:type '(alist :key-type symbol
|
|
:value-type (group string string symbol face)))
|
|
|
|
(defcustom ess-bp-inactive-spec
|
|
'(inactive "##" filled-square ess-bp-fringe-inactive-face)
|
|
"List giving the inactive breakpoint specifications."
|
|
;; List format is identical to that of the elements of
|
|
;; `ess-bp-type-spec-alist' except that the second element giving
|
|
;; the R expression is meaningless here." ;;fixme: second element is missing make it nil for consistency with all other specs
|
|
:group 'ess-debug
|
|
:type 'list)
|
|
|
|
(defcustom ess-bp-conditional-spec
|
|
'(conditional "browser(expr={%s})" "CB[ %s ]>\n" question-mark ess-bp-fringe-browser-face)
|
|
"List giving the conditional breakpoint specifications.
|
|
List format is identical to that of the elements of
|
|
`ess-bp-type-spec-alist'. User is asked for the conditional
|
|
expression to be replaced instead of %s in the second and third
|
|
elements of the specifications."
|
|
:group 'ess-debug
|
|
:type 'list)
|
|
|
|
(defcustom ess-bp-logger-spec
|
|
'(logger ".ess_log_eval('%s')" "L[ \"%s\" ]>\n" hollow-square ess-bp-fringe-logger-face)
|
|
"List giving the loggers specifications.
|
|
List format is identical to that of `ess-bp-type-spec-alist'."
|
|
:group 'ess-debug
|
|
:type 'list)
|
|
|
|
|
|
(defun ess-bp-get-bp-specs (type &optional condition no-error)
|
|
"Get specs for TYPE."
|
|
(let ((spec-alist (cond
|
|
((eq type 'conditional)
|
|
(let ((tl (copy-sequence ess-bp-conditional-spec)))
|
|
(when (eq (length condition) 0)
|
|
(setq condition "TRUE"))
|
|
(setcar (cdr tl) (format (cadr tl) condition))
|
|
(setcar (cddr tl) (format (caddr tl) condition))
|
|
(list tl)))
|
|
((eq type 'logger)
|
|
(let ((tl (copy-sequence ess-bp-logger-spec)))
|
|
(when (eq (length condition) 0)
|
|
(setq condition "watchLog"))
|
|
(setcar (cdr tl) (format (cadr tl) condition))
|
|
(setcar (cddr tl) (format (caddr tl) condition))
|
|
(list tl)))
|
|
(t (copy-sequence ess-bp-type-spec-alist)))))
|
|
(or (assoc type spec-alist)
|
|
(if no-error
|
|
nil
|
|
(error "Undefined breakpoint type %s" type)))))
|
|
|
|
(defun ess-bp-create (type &optional condition no-error)
|
|
"Set breakpoint for the current line.
|
|
Returns the beginning position of the hidden text."
|
|
(let* ((bp-specs (ess-bp-get-bp-specs type condition no-error))
|
|
(init-pos (point-marker))
|
|
(fringe-bitmap (nth 3 bp-specs))
|
|
(fringe-face (nth 4 bp-specs))
|
|
(displ-string (nth 2 bp-specs))
|
|
(bp-id (format "\"@%s@\""
|
|
(setq ess--bp-identifier (1+ ess--bp-identifier))))
|
|
(bp-command (concat (format (nth 1 bp-specs) bp-id)
|
|
"##:ess-bp-end:##\n"))
|
|
(dummy-string (format "##:ess-bp-start::%s@%s:##\n" (car bp-specs) condition))
|
|
insertion-pos)
|
|
(when bp-specs
|
|
(set-marker init-pos (1+ init-pos))
|
|
(setq displ-string (propertize displ-string
|
|
'face fringe-face
|
|
'font-lock-face fringe-face))
|
|
(setq bp-command (propertize bp-command
|
|
'ess-bp t
|
|
'bp-id bp-id
|
|
'bp-active t
|
|
'cursor-intangible 'ess-bp
|
|
'rear-nonsticky '(cursor-intangible ess-bp bp-type)
|
|
'bp-type type
|
|
'bp-substring 'command
|
|
'display displ-string))
|
|
(setq dummy-string (propertize
|
|
(ess-tracebug--propertize dummy-string fringe-bitmap fringe-face "*")
|
|
'ess-bp t
|
|
'cursor-intangible 'ess-bp
|
|
'bp-type type
|
|
'bp-substring 'dummy))
|
|
(ess-tracebug--set-left-margin)
|
|
(back-to-indentation)
|
|
(setq insertion-pos (point) )
|
|
(insert (concat dummy-string bp-command))
|
|
(indent-for-tab-command)
|
|
(goto-char (1- init-pos)) ;; sort of save-excursion
|
|
insertion-pos)))
|
|
|
|
(defun ess-bp-recreate-all ()
|
|
"Internal function to recreate all bp."
|
|
(save-excursion
|
|
(save-restriction
|
|
(with-silent-modifications
|
|
(cursor-intangible-mode)
|
|
(widen)
|
|
(goto-char (point-min))
|
|
(while (re-search-forward
|
|
"\\(##:ess-bp-start::\\(.*\\):##\n\\)\\(.+##:ess-bp-end:##\n\\)" nil t)
|
|
(let ((dum-beg (match-beginning 1))
|
|
(dum-end (match-end 1))
|
|
(comm-beg (match-beginning 3))
|
|
(comm-end (match-end 3))
|
|
(type (match-string 2))
|
|
(bp-command (match-string 3))
|
|
bp-id dum-props condition)
|
|
(when (string-match "^\\(\\w+\\)@\\(.*\\)\\'" type)
|
|
(setq condition (match-string 2 type))
|
|
(setq type (match-string 1 type)))
|
|
(setq bp-id
|
|
(if (string-match "\"@[0-9]+@\"" bp-command)
|
|
(match-string 0 bp-command)
|
|
(setq ess--bp-identifier (1+ ess--bp-identifier))))
|
|
(setq type (intern type))
|
|
(let* ((bp-specs (ess-bp-get-bp-specs type condition t))
|
|
(displ-string (nth 2 bp-specs))
|
|
(fringe-face (nth 4 bp-specs))
|
|
(fringe-bitmap (nth 3 bp-specs)))
|
|
(when bp-specs
|
|
(setq displ-string (propertize displ-string
|
|
'face fringe-face
|
|
'font-lock-face fringe-face))
|
|
(add-text-properties comm-beg comm-end
|
|
(list 'ess-bp t
|
|
'bp-id bp-id
|
|
'cursor-intangible 'ess-bp
|
|
'rear-nonsticky '(cursor-intangible ess-bp bp-type)
|
|
'bp-type type
|
|
'bp-substring 'command
|
|
'display displ-string))
|
|
(setq dum-props
|
|
(if window-system
|
|
(list 'display (list 'left-fringe fringe-bitmap fringe-face))
|
|
(list 'display (list '(margin left-margin)
|
|
(propertize "dummy"
|
|
'font-lock-face fringe-face
|
|
'face fringe-face)))))
|
|
(add-text-properties dum-beg dum-end
|
|
(append dum-props
|
|
(list 'ess-bp t
|
|
'cursor-intangible 'ess-bp
|
|
'bp-type type
|
|
'bp-substring 'dummy)))
|
|
;; (when comment-beg
|
|
;; (add-text-properties comment-beg comment-end
|
|
;; (list 'ess-bp t
|
|
;; 'bp-id bp-id
|
|
;; 'cursor-intangible 'ess-bp
|
|
;; 'display (propertize (nth 1 ess-bp-inactive-spec) 'face fringe-face)
|
|
;; 'bp-type type
|
|
;; 'bp-substring 'comment)))
|
|
))))))))
|
|
|
|
(add-hook 'ess-r-mode-hook 'ess-bp-recreate-all)
|
|
|
|
|
|
(defun ess-bp-get-bp-position-nearby ()
|
|
"Get nearby break points.
|
|
Return the cons (beg . end) of breakpoint limit points closest to
|
|
the current position. Only currently visible region of the buffer
|
|
is searched. This command is intended for use in interactive
|
|
commands like `ess-bp-toggle-state' and `ess-bp-kill'. Use
|
|
`ess-bp-previous-position' in programs."
|
|
(interactive)
|
|
(let* ((pos-end (if (get-char-property (1- (point)) 'ess-bp)
|
|
(point)
|
|
(previous-single-property-change (point) 'ess-bp nil (window-start))))
|
|
(pos-start (if (get-char-property (point) 'ess-bp) ;;check for bobp
|
|
(point)
|
|
(next-single-property-change (point) 'ess-bp nil (window-end))))
|
|
dist-up dist-down)
|
|
(unless (eq pos-end (window-start))
|
|
(setq dist-up (- (line-number-at-pos (point))
|
|
(line-number-at-pos pos-end))))
|
|
(unless (eq pos-start (window-end))
|
|
(setq dist-down (- (line-number-at-pos pos-start)
|
|
(line-number-at-pos (point)))))
|
|
(if (and dist-up dist-down)
|
|
(if (< dist-up dist-down)
|
|
(cons (previous-single-property-change pos-end 'ess-bp nil (window-start)) pos-end)
|
|
(cons pos-start (next-single-property-change pos-start 'ess-bp nil (window-end))))
|
|
(if dist-up
|
|
(cons (previous-single-property-change pos-end 'ess-bp nil (window-start)) pos-end)
|
|
(if dist-down
|
|
(cons pos-start (next-single-property-change pos-start 'ess-bp nil (window-end))))))))
|
|
|
|
|
|
(defun ess-bp-previous-position ()
|
|
"Get previous breakpoints.
|
|
Return the cons (beg . end) of breakpoint limit points closest
|
|
to the current position, nil if not found."
|
|
(let* ( (pos-end (if (get-char-property (1- (point)) 'ess-bp)
|
|
(point)
|
|
(previous-single-property-change (point) 'ess-bp ))))
|
|
(if pos-end
|
|
(cons (previous-single-property-change pos-end 'ess-bp) pos-end))))
|
|
|
|
(defun ess-bp-set ()
|
|
"Set a breakpoint."
|
|
(interactive)
|
|
(let* ((pos (ess-bp-get-bp-position-nearby))
|
|
(same-line (and pos
|
|
(<= (point-at-bol) (cdr pos))
|
|
(>= (point-at-eol) (car pos))))
|
|
(types ess-bp-type-spec-alist)
|
|
(ev last-command-event)
|
|
(com-char (event-basic-type ev))
|
|
bp-type)
|
|
(when same-line
|
|
;; set bp-type to next type in types
|
|
(setq bp-type (get-text-property (car pos) 'bp-type))
|
|
(setq types (cdr (member (assq bp-type types) types))) ; nil if bp-type is last in the list
|
|
(when (null types)
|
|
(setq types ess-bp-type-spec-alist))
|
|
(ess-bp-kill)
|
|
(indent-for-tab-command))
|
|
;; skip contextual bps
|
|
(while (and (nth 5 (car types))
|
|
(not (funcall (nth 5 (car types)))))
|
|
(pop types))
|
|
(setq bp-type (pop types))
|
|
(ess-bp-create (car bp-type))
|
|
(while (eq (event-basic-type (setq ev (read-event (format "'%c' to cycle" com-char))))
|
|
com-char)
|
|
(if (null types) (setq types ess-bp-type-spec-alist))
|
|
(ess-bp-kill)
|
|
;; skip contextual bps
|
|
(while (and (nth 5 (car types))
|
|
(not (funcall (nth 5 (car types)))))
|
|
(pop types))
|
|
(setq bp-type (pop types))
|
|
(ess-bp-create (car bp-type))
|
|
(indent-for-tab-command))
|
|
(push ev unread-command-events)))
|
|
|
|
|
|
(defun ess-bp-set-conditional (condition)
|
|
(interactive "sBreakpoint condition: ")
|
|
(ess-bp-create 'conditional condition)
|
|
(indent-for-tab-command))
|
|
|
|
(defun ess-bp-set-logger (name)
|
|
(interactive "sLogger name : ")
|
|
(ess-bp-create 'logger name)
|
|
(indent-for-tab-command))
|
|
|
|
(defun ess-bp-kill (&optional interactive?)
|
|
"Remove the breakpoint nearby."
|
|
(interactive "p")
|
|
(let ((pos (ess-bp-get-bp-position-nearby))
|
|
(init-pos (make-marker)))
|
|
(if (null pos)
|
|
(if interactive? (message "No breakpoints nearby"))
|
|
(if (eq (point) (point-at-eol))
|
|
(goto-char (1- (point)))) ;; work-around for issue 3
|
|
(set-marker init-pos (point))
|
|
(goto-char (car pos))
|
|
(delete-region (car pos) (cdr pos))
|
|
(indent-for-tab-command)
|
|
(goto-char init-pos)
|
|
(if (eq (point) (point-at-eol)) (forward-char)))))
|
|
|
|
(defun ess-bp-kill-all nil
|
|
"Delete all breakpoints in current buffer."
|
|
(interactive)
|
|
(let ((count 0)
|
|
(init-pos (make-marker))
|
|
pos)
|
|
(set-marker init-pos (1+ (point)))
|
|
(save-excursion ;; needed if error
|
|
(goto-char (point-max))
|
|
(while (setq pos (ess-bp-previous-position))
|
|
(goto-char (car pos))
|
|
(delete-region (car pos) (cdr pos))
|
|
(indent-for-tab-command)
|
|
(setq count (1+ count)))
|
|
(if (eq count 1)
|
|
(message "Killed 1 breakpoint")
|
|
(message "Killed %d breakpoint(s)" count)))
|
|
(goto-char (1- init-pos))))
|
|
|
|
|
|
(defun ess-bp-toggle-state ()
|
|
"Toggle the breakpoint between active and inactive states.
|
|
|
|
For standard breakpoints, the effect of this command is
|
|
immediate, that is you don't need to source your code and it
|
|
works even in the process of debugging.
|
|
|
|
For loggers, recover and conditional breakpoints this command
|
|
just comments the breakpoint in the source file.
|
|
|
|
If there is no active R session, this command triggers an error."
|
|
(interactive)
|
|
(unless (and ess-local-process-name
|
|
(get-process ess-local-process-name))
|
|
(error "No R session in this buffer"))
|
|
(save-excursion
|
|
(let ((pos (ess-bp-get-bp-position-nearby))
|
|
(fringe-face (nth 3 ess-bp-inactive-spec))
|
|
(cursor-sensor-inhibit 'ess-bp-toggle-state)
|
|
bp-id bp-specs beg-pos-command)
|
|
(if (null pos)
|
|
(message "No breakpoints in the visible region")
|
|
(goto-char (car pos))
|
|
(setq beg-pos-command (previous-single-property-change
|
|
(cdr pos) 'bp-substring nil (car pos))
|
|
bp-id (get-char-property beg-pos-command 'bp-id))
|
|
(goto-char beg-pos-command)
|
|
(if (get-char-property beg-pos-command 'bp-active)
|
|
(progn
|
|
(put-text-property (car pos) beg-pos-command ;; dummy display change
|
|
'display (list 'left-fringe (nth 2 ess-bp-inactive-spec) fringe-face))
|
|
(put-text-property beg-pos-command (cdr pos)
|
|
'bp-active nil)
|
|
(ess-command (format ".ESSBP.[[%s]] <- TRUE\n" bp-id)))
|
|
(setq bp-specs (assoc (get-text-property (point) 'bp-type) ess-bp-type-spec-alist))
|
|
(put-text-property beg-pos-command (cdr pos)
|
|
'bp-active t)
|
|
(put-text-property (car pos) beg-pos-command
|
|
'display (list 'left-fringe (nth 3 bp-specs) (nth 4 bp-specs)))
|
|
(ess-command (format ".ESSBP.[[%s]] <- NULL\n" bp-id))
|
|
;; (insert (propertize "##"
|
|
;; 'ess-bp t
|
|
;; 'cursor-intangible 'ess-bp
|
|
;; 'display (propertize (nth 1 ess-bp-inactive-spec) 'face fringe-face)
|
|
;; 'bp-type (get-char-property (point) 'bp-type)
|
|
;; 'bp-substring 'comment))
|
|
)))))
|
|
|
|
|
|
(defun ess-bp-make-visible ()
|
|
"Make bp text visible."
|
|
(interactive)
|
|
(let ((pos (ess-bp-get-bp-position-nearby)))
|
|
(set-text-properties (car pos) (cdr pos) (list 'display nil))))
|
|
|
|
|
|
|
|
(defun ess-bp-next nil
|
|
"Goto next breakpoint."
|
|
(interactive)
|
|
(when-let ((bp-pos (next-single-property-change (point) 'ess-bp)))
|
|
(save-excursion
|
|
(goto-char bp-pos)
|
|
(when (get-text-property (1- (point)) 'ess-bp)
|
|
(setq bp-pos (next-single-property-change bp-pos 'ess-bp))))
|
|
(if bp-pos
|
|
(goto-char bp-pos)
|
|
(message "No breakpoints found"))))
|
|
|
|
|
|
(defun ess-bp-previous nil
|
|
"Goto previous breakpoint."
|
|
(interactive)
|
|
(if-let ((bp-pos (previous-single-property-change (point) 'ess-bp)))
|
|
(goto-char (or (previous-single-property-change bp-pos 'ess-bp)
|
|
bp-pos))
|
|
(message "No breakpoints before the point found")))
|
|
|
|
;;;_ + WATCH
|
|
|
|
(defvar ess-watch-command
|
|
;; assumes that every expression is a structure of length 1 as returned by parse.
|
|
".ess_watch_eval()\n")
|
|
|
|
(if (fboundp 'define-fringe-bitmap) ;;not clear to me why is this not bound in SSH session? - :TODO check
|
|
(define-fringe-bitmap 'current-watch-bar
|
|
[#b00001100] nil nil '(top t)))
|
|
|
|
(defun ess-tracebug--set-left-margin ()
|
|
"Set the margin on non-X displays."
|
|
(unless window-system
|
|
(when (= left-margin-width 0)
|
|
(setq left-margin-width 1)
|
|
(set-window-buffer (selected-window) (current-buffer)))))
|
|
|
|
(define-derived-mode ess-watch-mode special-mode "ESS watch"
|
|
"Major mode in `ess-watch' window."
|
|
:group 'ess-tracebug
|
|
(let ((cur-block (max 1 (ess-watch-block-at-point)))
|
|
(dummy-string
|
|
(ess-tracebug--propertize "|" 'current-watch-bar 'font-lock-keyword-face)))
|
|
(ess-tracebug--set-left-margin)
|
|
(setq-local revert-buffer-function 'ess-watch-revert-buffer)
|
|
(turn-on-font-lock)
|
|
(setq ess-watch-current-block-overlay
|
|
(make-overlay (point-min) (point-max)))
|
|
(overlay-put ess-watch-current-block-overlay 'line-prefix dummy-string)
|
|
(overlay-put ess-watch-current-block-overlay 'face 'ess-watch-current-block-face)
|
|
(ess-watch-set-current cur-block) ;;
|
|
(require 'face-remap)
|
|
;; scale the font
|
|
(setq text-scale-mode-amount ess-watch-scale-amount)
|
|
(text-scale-mode)))
|
|
|
|
(defun ess-watch ()
|
|
"Run `ess-watch-mode' on R objects.
|
|
This is the trigger function. See documentation of
|
|
`ess-watch-mode' for more information."
|
|
(interactive)
|
|
(ess-force-buffer-current)
|
|
(let ((wbuf (get-buffer-create ess-watch-buffer))
|
|
(pname ess-local-process-name))
|
|
(pop-to-buffer wbuf
|
|
;; not strongly dedicated
|
|
'(nil . ((dedicated . 1))))
|
|
(setq ess-local-process-name pname)
|
|
(ess-watch-mode)
|
|
;; evals the ess-command and displays the buffer if not visible
|
|
(ess-watch-refresh-buffer-visibly wbuf)))
|
|
|
|
|
|
(defun ess-watch-refresh-buffer-visibly (wbuf &optional sleep no-prompt-check)
|
|
"Eval `ess-watch-command' and direct the output into the WBUF.
|
|
Call `ess-watch-buffer-show' to make the buffer visible, without
|
|
selecting it. SLEEP and NO-PROMPT-CHECK get passed to `ess-command'.
|
|
|
|
This function is used for refreshing the watch window after each step during
|
|
the debugging."
|
|
;; assumes that the ess-watch-mode is on!!
|
|
;; particularly ess-watch-current-block-overlay is installed
|
|
(ess-watch-buffer-show wbuf) ;; if visible do nothing
|
|
(let ((pname ess-local-process-name)) ;; watch might be used from different dialects, need to reset
|
|
(with-current-buffer wbuf
|
|
(let ((curr-block (max 1 (ess-watch-block-at-point))) ;;can be 0 if
|
|
(inhibit-read-only t))
|
|
(when pname
|
|
(setq ess-local-process-name pname))
|
|
(ess-command ess-watch-command wbuf sleep no-prompt-check)
|
|
;; delete the ++++++> line ;; not very reliable but works fine so far.
|
|
(goto-char (point-min))
|
|
(delete-region (point-at-bol) (+ 1 (point-at-eol)))
|
|
(ess-watch-set-current curr-block)
|
|
(set-window-point (get-buffer-window wbuf) (point))))))
|
|
|
|
(defun ess-watch-buffer-show (buffer-or-name)
|
|
"Make watch buffer BUFFER-OR-NAME visible, and position accordingly.
|
|
If already visible, do nothing.
|
|
|
|
Currently the only positioning rule implemented is to split the R
|
|
process window in half. The behavior is controlled by
|
|
`split-window-sensibly' with parameters `split-height-threshold'
|
|
and `split-width-threshold' replaced by
|
|
`ess-watch-height-threshold' and `ess-watch-width-threshold'
|
|
respectively."
|
|
(interactive)
|
|
(unless (get-buffer-window ess-watch-buffer 'visible)
|
|
(save-selected-window
|
|
(ess-switch-to-ESS t)
|
|
(let* ((split-width-threshold (or ess-watch-width-threshold
|
|
split-width-threshold))
|
|
(split-height-threshold (or ess-watch-height-threshold
|
|
split-height-threshold))
|
|
(win (split-window-sensibly (selected-window))))
|
|
(if win
|
|
(set-window-buffer win buffer-or-name)
|
|
(display-buffer buffer-or-name) ;; resort to usual mechanism if could not split
|
|
)))))
|
|
|
|
|
|
(defun ess-watch-revert-buffer (_ignore _noconfirm)
|
|
"Update the watch buffer.
|
|
Arguments IGNORE and NOCONFIRM currently not used."
|
|
(ess-watch)
|
|
(message "Watch reverted"))
|
|
|
|
|
|
(defface ess-watch-current-block-face
|
|
'((default (:inherit highlight)))
|
|
"Face used to highlight current watch block."
|
|
:group 'ess-debug)
|
|
|
|
(defvar ess-watch-start-block "@----" ;; fixme: make defcustom and modify the injected command correspondingly
|
|
"String indicating the beginning of a block in watch buffer."
|
|
;; :group 'ess-debug
|
|
;; :type 'string
|
|
)
|
|
|
|
(defvar ess-watch-start-expression "@---:"
|
|
"String indicating the beginning of an R expression in watch buffer."
|
|
;; :group 'ess-debug
|
|
;; :type 'string
|
|
)
|
|
|
|
(defun ess-watch-block-limits-at-point ()
|
|
"Return start and end positions of the watch block."
|
|
(interactive)
|
|
(save-excursion
|
|
(let ((curr (point))
|
|
start-pos end-pos)
|
|
(end-of-line)
|
|
(setq start-pos
|
|
(if (re-search-backward ess-watch-start-block nil t )
|
|
(point)
|
|
(point-min)))
|
|
(goto-char curr)
|
|
(beginning-of-line)
|
|
(setq end-pos
|
|
(if (re-search-forward ess-watch-start-block nil t)
|
|
(match-beginning 0)
|
|
(point-max)))
|
|
(list start-pos end-pos))))
|
|
|
|
(defun ess-watch-block-at-point ()
|
|
"Return the current block's order count, 0 if no block was found."
|
|
(save-excursion
|
|
(let ((cur-point (point))
|
|
(count 0))
|
|
(goto-char (point-min))
|
|
(while (re-search-forward ess-watch-start-block cur-point t)
|
|
(setq count (1+ count)))
|
|
count)))
|
|
|
|
(defun ess-watch-set-current (nr)
|
|
"Move the overlay over the block with count NR in current watch buffer."
|
|
(goto-char (point-min))
|
|
(re-search-forward ess-watch-start-expression nil t nr)
|
|
(goto-char (match-end 0))
|
|
(apply 'move-overlay ess-watch-current-block-overlay (ess-watch-block-limits-at-point)))
|
|
|
|
|
|
(defun ess-watch--make-alist ()
|
|
"Create an alist of expressions from the current watch buffer.
|
|
Each element of assoc list is of the form (pos name expr) where
|
|
pos is an unique integer identifying watch blocks by position,
|
|
name is a string giving the name of expression block, expr is a
|
|
string giving the actual R expression."
|
|
(interactive)
|
|
(save-excursion
|
|
(let* ((reg-name (concat "^" ess-watch-start-block " *\\(\\S-*\\).*$"))
|
|
(reg-expr (concat "^" ess-watch-start-expression "\\s-*\\(.*\\)$"))
|
|
(reg-all (concat "\\(" reg-name "\\)\n\\(" reg-expr "\\)"))
|
|
(pos 0) wal name expr)
|
|
(goto-char (point-min))
|
|
(while (re-search-forward reg-all nil t)
|
|
(setq pos (+ 1 pos))
|
|
(setq name (match-string-no-properties 2))
|
|
(setq expr (match-string-no-properties 4))
|
|
(if (not (eq (string-to-number name) 0)) ;;if number of any kind set the name to ""
|
|
(setq name ""))
|
|
(setq wal
|
|
(append wal (list (list pos name expr)))))
|
|
wal)))
|
|
|
|
(defun ess-watch--parse-assoc (al)
|
|
"Return a string of the form 'assign(\".ess_watch_expressions\", list(a = parse(expr_a), b= parse(expr_b)), envir = .GlobalEnv)'
|
|
ready to be send to R process. AL is an association list as return by `ess-watch--make-alist'"
|
|
(concat ".ess_watch_assign_expressions(list("
|
|
(mapconcat (lambda (el)
|
|
(if (> (length (cadr el) ) 0)
|
|
(concat "`" (cadr el) "` = parse(text = '" (caddr el) "')")
|
|
(concat "parse(text = '" (caddr el) "')")))
|
|
al ", ")
|
|
"))\n"))
|
|
|
|
(defun ess-watch--install-.ess_watch_expressions ()
|
|
;; used whenever watches are added/deleted/modified from the watch
|
|
;; buffer. this is the only way to insert expressions into
|
|
;; .ess_watch_expressions object in R. Assumes R watch being the current
|
|
;; buffer, otherwise will most likely install empty list.
|
|
(interactive)
|
|
(process-send-string (ess-get-process ess-current-process-name)
|
|
(ess-watch--parse-assoc (ess-watch--make-alist)))
|
|
;;TODO: delete the prompt at the end of proc buffer TODO: defun ess-send-string!!
|
|
(sleep-for 0.05) ;; need here, if ess-command is used immediately after, for some weird reason the process buffer will not be changed
|
|
)
|
|
|
|
|
|
(defun ess-watch-quit ()
|
|
"Quit (kill) the watch buffer.
|
|
If watch buffer exists, it is displayed during the debug
|
|
process. The only way to avoid the display, is to kill the
|
|
buffer."
|
|
(interactive)
|
|
(kill-buffer) ;; dedicated, window is deleted unless not the only one
|
|
)
|
|
|
|
;;;_ + MOTION
|
|
(defun ess-watch-next-block (&optional n)
|
|
"Move the overlay over the next block.
|
|
Optional N if supplied gives the number of steps forward `backward-char'."
|
|
(interactive "P")
|
|
(setq n (prefix-numeric-value n))
|
|
(goto-char (overlay-end ess-watch-current-block-overlay))
|
|
(unless (re-search-forward ess-watch-start-expression nil t n)
|
|
(goto-char (point-min)) ;;circular but always moves to start!
|
|
(re-search-forward ess-watch-start-expression nil t 1))
|
|
(apply 'move-overlay ess-watch-current-block-overlay (ess-watch-block-limits-at-point)))
|
|
|
|
(defun ess-watch-previous-block (&optional n)
|
|
"Move the overlay over the previous block.
|
|
Optional N if supplied gives the number of backward steps."
|
|
(interactive "P")
|
|
(setq n (prefix-numeric-value n))
|
|
(goto-char (overlay-start ess-watch-current-block-overlay))
|
|
(unless (re-search-backward ess-watch-start-expression nil t n)
|
|
(goto-char (point-max)) ;;circular but always moves to last!
|
|
(re-search-backward ess-watch-start-expression nil t 1))
|
|
(goto-char (match-end 0))
|
|
(apply 'move-overlay ess-watch-current-block-overlay (ess-watch-block-limits-at-point)))
|
|
|
|
;;;_ + BLOCK MANIPULATION and EDITING
|
|
(defun ess-watch-rename ()
|
|
"Rename the currently selected watch block."
|
|
(interactive)
|
|
(end-of-line)
|
|
(unless (re-search-backward ess-watch-start-block nil t)
|
|
(error "Can not find a watch block"))
|
|
(let ((reg-name (concat ess-watch-start-block " *\\(\\S-*\\).*$"))
|
|
name start end)
|
|
;; (reg-expr (concat "^" ess-watch-start-expression "\\s-*\\(.*\\)$"))
|
|
;; (reg-all (concat "\\(" reg-name "\\)\n\\(" reg-expr "\\)"))
|
|
;; (pos 0) wal name expr)
|
|
(unless (re-search-forward reg-name (point-at-eol) t)
|
|
(error "Can not find the name substring in the current watch block "))
|
|
(setq name (match-string-no-properties 1))
|
|
(setq start (match-beginning 1))
|
|
(setq end (match-end 1))
|
|
(goto-char start)
|
|
;; TODO: highlight the name in R-watch here
|
|
(setq name (read-string (concat "New name (" name "): ") nil nil name) )
|
|
(setq buffer-read-only nil)
|
|
(delete-region start end)
|
|
(insert name)
|
|
(setq buffer-read-only t)
|
|
(ess-watch--install-.ess_watch_expressions)
|
|
(ess-watch-refresh-buffer-visibly (current-buffer))))
|
|
|
|
(defun ess-watch-edit-expression ()
|
|
"Edit in the minibuffer the R expression from the current watch block."
|
|
(interactive)
|
|
(end-of-line)
|
|
(unless (re-search-backward ess-watch-start-block nil 1)
|
|
(error "Can not find a watch block"))
|
|
(let ((reg-expr (concat ess-watch-start-expression " *\\(.*\\)$"))
|
|
expr start end)
|
|
(unless (re-search-forward reg-expr nil t)
|
|
(error "Can not find an expression string in the watch block"))
|
|
(setq expr (match-string-no-properties 1))
|
|
(setq start (match-beginning 1))
|
|
(setq end (match-end 1))
|
|
(goto-char start)
|
|
;; TODO: highlight the name in R-watch here
|
|
(setq expr (read-string "New expression: " expr nil expr) )
|
|
(setq buffer-read-only nil)
|
|
(delete-region start end)
|
|
(insert expr)
|
|
(setq buffer-read-only t)
|
|
(ess-watch--install-.ess_watch_expressions)
|
|
(ess-watch-refresh-buffer-visibly (current-buffer))))
|
|
|
|
(defun ess-watch-add ()
|
|
"Ask for new R expression and name and append it to the end of the list of watch expressions."
|
|
(interactive)
|
|
(let (nr expr name)
|
|
(goto-char (point-max))
|
|
(setq nr (number-to-string (1+ (ess-watch-block-at-point))))
|
|
(setq name nr)
|
|
;; (setq name (read-string (concat "Name (" nr "):") nil nil nr )) ;;this one is quite annoying and not really needed than for logging
|
|
(setq expr (read-string "New expression: " nil nil "\"Empty watch!\""))
|
|
(setq buffer-read-only nil)
|
|
(insert (concat "\n" ess-watch-start-block " " name " -@\n" ess-watch-start-expression " " expr "\n"))
|
|
(setq buffer-read-only t)
|
|
(ess-watch--install-.ess_watch_expressions)))
|
|
|
|
(defun ess-watch-insert ()
|
|
"Ask for new R expression and name and insert it in front of current watch block."
|
|
(interactive)
|
|
(let (nr expr name)
|
|
(setq nr (number-to-string (ess-watch-block-at-point)))
|
|
(setq name nr)
|
|
;; (setq name (read-string (concat "Name (" nr "):") nil nil nr ))
|
|
(setq expr (read-string "New expression: " nil nil "\"Empty watch!\""))
|
|
(re-search-backward ess-watch-start-block nil 1) ;;point-min if not found
|
|
(setq buffer-read-only nil)
|
|
(insert (concat "\n" ess-watch-start-block " " name " -@\n" ess-watch-start-expression " " expr "\n"))
|
|
(setq buffer-read-only t)
|
|
(ess-watch--install-.ess_watch_expressions)))
|
|
|
|
(defun ess-watch-move-up ()
|
|
"Move the current block up."
|
|
(interactive)
|
|
(let ((nr (ess-watch-block-at-point))
|
|
wbl)
|
|
(when (> nr 1)
|
|
(setq buffer-read-only nil)
|
|
(setq wbl (apply 'delete-and-extract-region (ess-watch-block-limits-at-point)))
|
|
(re-search-backward ess-watch-start-block nil t 1) ;; current block was deleted, point is at the end of previous block
|
|
(insert wbl)
|
|
(ess-watch--install-.ess_watch_expressions)
|
|
(setq buffer-read-only t))))
|
|
|
|
|
|
(defun ess-watch-move-down ()
|
|
"Move the current block down."
|
|
(interactive)
|
|
(let ((nr (ess-watch-block-at-point))
|
|
(nr-all (save-excursion (goto-char (point-max))
|
|
(ess-watch-block-at-point)))
|
|
wbl)
|
|
(when (< nr nr-all)
|
|
(setq buffer-read-only nil)
|
|
(setq wbl (apply 'delete-and-extract-region (ess-watch-block-limits-at-point)))
|
|
(end-of-line)
|
|
(when (re-search-forward ess-watch-start-block nil t 1) ;; current block was deleted, point is at the end of previous block or point-max
|
|
(goto-char (match-beginning 0)))
|
|
(insert wbl)
|
|
(ess-watch--install-.ess_watch_expressions)
|
|
(setq buffer-read-only t))))
|
|
|
|
(defun ess-watch-kill ()
|
|
"Kill the current block."
|
|
(interactive)
|
|
(setq buffer-read-only nil)
|
|
(apply 'delete-region (ess-watch-block-limits-at-point))
|
|
(ess-watch--install-.ess_watch_expressions))
|
|
|
|
;;;_ + Debug/Undebug at point
|
|
(defun ess--dbg-get-signatures (method)
|
|
"Get signatures for the method METHOD."
|
|
(let ((tbuffer (get-buffer-create " *ess-command-output*")); initial space: disable-undo
|
|
signatures)
|
|
(save-excursion
|
|
(ess-if-verbose-write (format "ess-get-signatures*(%s).. " method))
|
|
(ess-command (concat "showMethods(\"" method "\")\n") tbuffer)
|
|
(message "%s" ess-local-process-name)
|
|
(message "%s" ess-current-process-name)
|
|
(ess-if-verbose-write " [ok] ..\n")
|
|
(set-buffer tbuffer)
|
|
(goto-char (point-min))
|
|
(if (not (re-search-forward "Function:" nil t))
|
|
(progn (ess-if-verbose-write "not seeing \"Function:\".. \n")
|
|
(error (buffer-string))
|
|
;; (error "Cannot trace method '%s' (Is it a primitive method which you have already traced?)" method)
|
|
)
|
|
;; (setq curr-point (point))
|
|
;; (while (re-search-forward ", " nil t) ;replace all ", " with ":" for better readability in completion buffers??
|
|
;; (replace-match ":"))
|
|
;; (goto-char curr-point)
|
|
(while (re-search-forward "^.+$" nil t)
|
|
(setq signatures (cons (match-string-no-properties 0) signatures))))
|
|
; (kill-buffer tbuffer)
|
|
)
|
|
signatures))
|
|
|
|
|
|
(defun ess-debug-flag-for-debugging ()
|
|
"Set the debugging flag on a function.
|
|
Ask the user for a function and if it turns to be generic, ask
|
|
for signature and trace it with browser tracer."
|
|
(interactive)
|
|
(ess-force-buffer-current "Process to use: ")
|
|
(let* ((tbuffer (get-buffer-create " *ess-command-output*")) ;; output buffer name is hard-coded in ess-inf.el
|
|
(pkg (ess-r-package-name))
|
|
(all-functions (ess-get-words-from-vector
|
|
(if pkg
|
|
(format ".ess_all_functions(c('%s'))\n" pkg)
|
|
".ess_all_functions()\n")))
|
|
(obj-at-point (ess-helpobjs-at-point--read-obj))
|
|
(default (and
|
|
obj-at-point
|
|
(let* ((reg (regexp-quote obj-at-point))
|
|
(matches (cl-loop for el in all-functions
|
|
if (string-match reg el) collect el)))
|
|
(car (sort matches (lambda (a b) (< (length a) (length b))))))))
|
|
(ufunc (ess-completing-read "Debug" all-functions
|
|
nil nil nil nil (or default obj-at-point)))
|
|
signature)
|
|
;; FIXME: Most of the following logic should be in R
|
|
(if (ess-boolean-command (format "as.character(isGeneric('%s'))\n" ufunc))
|
|
|
|
;; it's S4 generic:
|
|
(save-excursion
|
|
;; ask for exact signature
|
|
(setq signature
|
|
(ess-completing-read (concat "Method for generic '" ufunc "'")
|
|
(ess--dbg-get-signatures ufunc) ;signal an error if not found
|
|
nil t nil nil "*default*"))
|
|
(if (equal signature "*default*")
|
|
;;debug, the default ufunc
|
|
(ess-command (format "trace('%s', tracer = browser)\n" ufunc) tbuffer)
|
|
(ess-command (format "trace('%s', tracer = browser, signature = c('%s'))\n" ufunc signature) tbuffer))
|
|
(with-current-buffer tbuffer
|
|
;; give appropriate message or error
|
|
(message "%s" (buffer-substring-no-properties (point-min) (point-max)))))
|
|
|
|
;;else, not an S4 generic
|
|
(when (ess-boolean-command (format "as.character(.knownS3Generics['%s'])\n" ufunc))
|
|
;; it's S3 generic:
|
|
(setq all-functions
|
|
(ess-get-words-from-vector
|
|
(format "local({gens<-methods('%s');as.character(gens[attr(gens, 'info')$visible])})\n" ufunc)))
|
|
(setq all-functions
|
|
;; cannot debug non-visible methods
|
|
(delq nil (mapcar (lambda (el)
|
|
(if (not (char-equal ?* (aref el (1- (length el))))) el))
|
|
all-functions)))
|
|
(setq ufunc (ess-completing-read (format "Method for S3 generic '%s'" ufunc)
|
|
(cons ufunc all-functions) nil t)))
|
|
(ess-command (format ".ess_dbg_flag_for_debuging('%s')\n" ufunc)))))
|
|
|
|
|
|
(defun ess-debug-unflag-for-debugging ()
|
|
"Prompt for the debugged/traced function or method and undebug/untrace it."
|
|
(interactive)
|
|
(let ((tbuffer (get-buffer-create " *ess-command-output*")); initial space: disable-undo\
|
|
(debugged (ess-get-words-from-vector
|
|
(if nil ;; FIXME: was checking `ess-developer-packages`
|
|
(format ".ess_dbg_getTracedAndDebugged(c('%s'))\n"
|
|
(mapconcat 'identity ess-developer-packages "', '"))
|
|
".ess_dbg_getTracedAndDebugged()\n")))
|
|
out-message fun def-val)
|
|
;; (prin1 debugged)
|
|
(if (eq (length debugged) 0)
|
|
(setq out-message "No debugged or traced functions/methods found")
|
|
(setq def-val (if (eq (length debugged) 1)
|
|
(car debugged)
|
|
"*ALL*"))
|
|
(setq fun (ess-completing-read "Undebug" debugged nil t nil nil def-val))
|
|
(if (equal fun "*ALL*" )
|
|
(ess-command (concat ".ess_dbg_UndebugALL(c(\"" (mapconcat 'identity debugged "\", \"") "\"))\n") tbuffer)
|
|
(ess-command (format ".ess_dbg_UntraceOrUndebug(\"%s\")\n" fun) tbuffer))
|
|
(with-current-buffer tbuffer
|
|
(if (= (point-max) 1) ;; not reliable TODO:
|
|
(setq out-message (format "Undebugged '%s' " fun))
|
|
(setq out-message (buffer-substring-no-properties (point-min) (point-max))) ;; untrace info or warning, or error occurred
|
|
)))
|
|
(message "%s" out-message)))
|
|
|
|
;;;_ * Kludges and Fixes
|
|
;;; delete-char and delete-backward-car do not delete whole intangible text
|
|
(defun ess--tracebug-delete-char (n &rest _)
|
|
"When deleting an intangible char, delete the whole intangible region.
|
|
Only do this when N is 1"
|
|
(when (and (ess-derived-mode-p)
|
|
(= n 1)
|
|
(get-text-property (point) 'cursor-intangible))
|
|
(kill-region (point) (or (next-single-property-change (point) 'cursor-intangible)
|
|
(point-max)))
|
|
(indent-according-to-mode)))
|
|
|
|
(advice-add 'delete-char :before-until #'ess--tracebug-delete-char)
|
|
|
|
(defun ess--tracebug-delete-backward-char (n &rest _)
|
|
"When deleting an intangible char, delete the whole intangible region.
|
|
Only do this when called interactively and N is 1"
|
|
(when (and (ess-derived-mode-p)
|
|
(= n 1)
|
|
(> (point) (point-min))
|
|
(get-text-property (1- (point)) 'cursor-intangible))
|
|
(kill-region (or (previous-single-property-change (point) 'cursor-intangible)
|
|
(point-min))
|
|
(point))))
|
|
|
|
(advice-add 'delete-backward-char :before-until #'ess--tracebug-delete-backward-char)
|
|
|
|
(provide 'ess-tracebug)
|
|
|
|
;;; ess-tracebug.el ends here
|