Add backtrace-mode and use it in the debugger, ERT and Edebug
* doc/lispref/debugging.texi (Using Debugger): Remove explanation of backtrace buffer. Refer to new node. (Backtraces): New node. (Debugger Commands): Refer to new node. Remove 'v'. * doc/lispref/edebug.texi (Edebug Misc): Refer to new node. * doc/misc/ert.texi (Running Tests Interactively): Refer to new node. * lisp/emacs-lisp-backtrace.el: New file. * test/lisp/emacs-lisp/backtrace-tests.el: New file. * lisp/emacs-lisp/debug.el: (debugger-buffer-state): New cl-defstruct. (debugger--restore-buffer-state): New function. (debug): Use a debugger-buffer-state object to save and restore buffer state. Fix bug#15749 by leaving an unused buffer in debugger-mode, empty, instead of in fundamental-mode, and then when reusing a buffer, not calling debugger-mode if the buffer is already in debugger-mode. (debugger-insert-backtrace): Remove. (debugger-setup-buffer): Use backtrace-mode. (debugger--insert-header): New function. (debugger-continue, debugger-return-value): Change check for flags to use backtrace-frames. (debugger-frame-number): Determine backtrace frame number from backtrace-frames. (debugger--locals-visible-p, debugger--insert-locals) (debugger--show-locals, debugger--hide-locals) (debugger-toggle-locals): Remove. (debugger-mode-map): Make a child of backtrace-mode-map. Move navigation commands to backtrace-mode-map. Bind 'q' to debugger-quit instead of top-level. Make Help Follow menu item call backtrace-help-follow-symbol. (debugger-mode): Derive from backtrace-mode. (debug-help-follow): Remove. Move body of this function to 'backtrace-help-follow-symbol' in backtrace.el. (debugger-quit): New function. * lisp/emacs-lisp/edebug.el (edebug-unwrap-results): Remove warning in docstring about circular results. (edebug-unwrap): Use pcase. (edebug-unwrap1): New function to unwrap circular objects. (edebug-unwrap*): Use it. (edebug--frame): New cl-defstruct. (edebug-backtrace): Call the buffer *Edebug Backtrace* and use backtrace-mode. Get the frames from edebug--backtrace-frames. (edebug--backtrace-frames, edebug--unwrap-and-add-info) (edebug--symbol-not-prefixed-p): New functions. * lisp/emacs-lisp/lisp-mode.el (lisp-el-font-lock-keywords-for-backtraces) (lisp-el-font-lock-keywords-for-backtraces-1) (lisp-el-font-lock-keywords-for-backtraces-2): New constants. * lisp/emacs-lisp/ert.el (ert--print-backtrace): Remove. (ert--run-test-debugger): Use backtrace-get-frames. (ert-run-tests-batch): Use backtrace-to-string. (ert-results-pop-to-backtrace-for-test-at-point): Use backtrace-mode. (ert--insert-backtrace-header): New function. * tests/lisp/emacs-lisp/ert-tests.el (ert-test--which-file): Use backtrace-frame slot accessor.
This commit is contained in:
parent
8a7620955b
commit
e09120d686
11 changed files with 1260 additions and 350 deletions
|
@ -81,7 +81,8 @@ debugger recursively. @xref{Recursive Editing}.
|
|||
* Function Debugging:: Entering it when a certain function is called.
|
||||
* Variable Debugging:: Entering it when a variable is modified.
|
||||
* Explicit Debug:: Entering it at a certain point in the program.
|
||||
* Using Debugger:: What the debugger does; what you see while in it.
|
||||
* Using Debugger:: What the debugger does.
|
||||
* Backtraces:: What you see while in the debugger.
|
||||
* Debugger Commands:: Commands used while in the debugger.
|
||||
* Invoking the Debugger:: How to call the function @code{debug}.
|
||||
* Internals of Debugger:: Subroutines of the debugger, and global variables.
|
||||
|
@ -392,32 +393,79 @@ this is not what you want, you can either set
|
|||
@code{eval-expression-debug-on-error} to @code{nil}, or set
|
||||
@code{debug-on-error} to @code{nil} in @code{debugger-mode-hook}.
|
||||
|
||||
@cindex current stack frame
|
||||
The backtrace buffer shows you the functions that are executing and
|
||||
their argument values. It also allows you to specify a stack frame by
|
||||
moving point to the line describing that frame. (A stack frame is the
|
||||
place where the Lisp interpreter records information about a particular
|
||||
invocation of a function.) The frame whose line point is on is
|
||||
considered the @dfn{current frame}. Some of the debugger commands
|
||||
operate on the current frame. If a line starts with a star, that means
|
||||
that exiting that frame will call the debugger again. This is useful
|
||||
for examining the return value of a function.
|
||||
|
||||
If a function name is underlined, that means the debugger knows
|
||||
where its source code is located. You can click with the mouse on
|
||||
that name, or move to it and type @key{RET}, to visit the source code.
|
||||
|
||||
The debugger itself must be run byte-compiled, since it makes
|
||||
assumptions about how many stack frames are used for the debugger
|
||||
itself. These assumptions are false if the debugger is running
|
||||
interpreted.
|
||||
assumptions about the state of the Lisp interpreter. These
|
||||
assumptions are false if the debugger is running interpreted.
|
||||
|
||||
@node Backtraces
|
||||
@subsection Backtraces
|
||||
@cindex backtrace buffer
|
||||
|
||||
Debugger mode is derived from Backtrace mode, which is also used to
|
||||
show backtraces by Edebug and ERT. (@pxref{Edebug} and @ref{Top,the
|
||||
ERT manual,, ert, ERT: Emacs Lisp Regression Testing})
|
||||
|
||||
@cindex stack frame
|
||||
The backtrace buffer shows you the functions that are executing and
|
||||
their argument values. When a backtrace buffer is created, it shows
|
||||
each stack frame on one, possibly very long, line. (A stack frame is
|
||||
the place where the Lisp interpreter records information about a
|
||||
particular invocation of a function.) The most recently called
|
||||
function will be at the top.
|
||||
|
||||
@cindex current stack frame
|
||||
In a backtrace you can specify a stack frame by moving point to a line
|
||||
describing that frame. The frame whose line point is on is considered
|
||||
the @dfn{current frame}.
|
||||
|
||||
If a function name is underlined, that means Emacs knows where its
|
||||
source code is located. You can click with the mouse on that name, or
|
||||
move to it and type @key{RET}, to visit the source code. You can also
|
||||
type @key{RET} while point is on any name of a function or variable
|
||||
which is not underlined, to see help information for that symbol in a
|
||||
help buffer, if any exists. The @code{xref-find-definitions} command,
|
||||
bound to @key{M-.}, can also be used on any identifier in a backtrace
|
||||
(@pxref{Looking Up Identifiers,,,emacs,Emacs manual}).
|
||||
|
||||
In backtraces, the tails of long lists and the ends of long strings,
|
||||
vectors or structures, as well as objects which are deeply nested,
|
||||
will be printed as underlined ``...''. You can click with the mouse
|
||||
on a ``...'', or type @key{RET} while point is on it, to show the part
|
||||
of the object that was hidden. To control how much abbreviation is
|
||||
done, customize @code{backtrace-line-length}.
|
||||
|
||||
Here is a list of commands for navigating and viewing backtraces:
|
||||
|
||||
@table @kbd
|
||||
@item v
|
||||
Toggle the display of local variables of the current stack frame.
|
||||
|
||||
@item p
|
||||
Move to the beginning of the frame, or to the beginning
|
||||
of the previous frame.
|
||||
|
||||
@item n
|
||||
Move to the beginning of the next frame.
|
||||
|
||||
@item +
|
||||
Add line breaks and indentation to the top-level Lisp form at point to
|
||||
make it more readable.
|
||||
|
||||
@item =
|
||||
Collapse the top-level Lisp form at point back to a single line.
|
||||
|
||||
@item #
|
||||
Toggle @code{print-circle} for the frame at point.
|
||||
|
||||
@end table
|
||||
|
||||
@node Debugger Commands
|
||||
@subsection Debugger Commands
|
||||
@cindex debugger command list
|
||||
|
||||
The debugger buffer (in Debugger mode) provides special commands in
|
||||
addition to the usual Emacs commands. The most important use of
|
||||
addition to the usual Emacs commands and to the Backtrace mode commands
|
||||
described in the previous section. The most important use of
|
||||
debugger commands is for stepping through code, so that you can see
|
||||
how control flows. The debugger can step through the control
|
||||
structures of an interpreted function, but cannot do so in a
|
||||
|
@ -427,6 +475,11 @@ the same function. (To do this, visit the source for the function and
|
|||
type @kbd{C-M-x} on its definition.) You cannot use the Lisp debugger
|
||||
to step through a primitive function.
|
||||
|
||||
Some of the debugger commands operate on the current frame. If a
|
||||
frame starts with a star, that means that exiting that frame will call the
|
||||
debugger again. This is useful for examining the return value of a
|
||||
function.
|
||||
|
||||
@c FIXME: Add @findex for the following commands? --xfq
|
||||
Here is a list of Debugger mode commands:
|
||||
|
||||
|
@ -502,8 +555,6 @@ Display a list of functions that will invoke the debugger when called.
|
|||
This is a list of functions that are set to break on entry by means of
|
||||
@code{debug-on-entry}.
|
||||
|
||||
@item v
|
||||
Toggle the display of local variables of the current stack frame.
|
||||
@end table
|
||||
|
||||
@node Invoking the Debugger
|
||||
|
|
|
@ -442,8 +442,8 @@ Redisplay the most recently known expression result in the echo area
|
|||
Display a backtrace, excluding Edebug's own functions for clarity
|
||||
(@code{edebug-backtrace}).
|
||||
|
||||
You cannot use debugger commands in the backtrace buffer in Edebug as
|
||||
you would in the standard debugger.
|
||||
@xref{Debugging,, Backtraces, elisp}, for the commands which work
|
||||
in a backtrace buffer.
|
||||
|
||||
The backtrace buffer is killed automatically when you continue
|
||||
execution.
|
||||
|
|
|
@ -273,9 +273,11 @@ moving point to it and typing @kbd{@key{RET}} jumps to its definition.
|
|||
@cindex backtrace of a failed test
|
||||
Pressing @kbd{r} re-runs the test near point on its own. Pressing
|
||||
@kbd{d} re-runs it with the debugger enabled. @kbd{.} jumps to the
|
||||
definition of the test near point (@kbd{@key{RET}} has the same effect if
|
||||
point is on the name of the test). On a failed test, @kbd{b} shows
|
||||
the backtrace of the failure.
|
||||
definition of the test near point (@kbd{@key{RET}} has the same effect
|
||||
if point is on the name of the test). On a failed test, @kbd{b} shows
|
||||
the backtrace of the failure. @xref{Debugging,, Backtraces, elisp,
|
||||
the Emacs Lisp Reference Manual}, for more information about
|
||||
backtraces.
|
||||
|
||||
@kindex l@r{, in ert results buffer}
|
||||
@kbd{l} shows the list of @code{should} forms executed in the test.
|
||||
|
|
31
etc/NEWS
31
etc/NEWS
|
@ -466,6 +466,14 @@ the shift key.
|
|||
*** Isearch now remembers the regexp-based search mode for words/symbols
|
||||
and case-sensitivity together with search strings in the search ring.
|
||||
|
||||
** Debugger
|
||||
|
||||
+++
|
||||
*** The Lisp Debugger is now based on 'backtrace-mode'.
|
||||
Backtrace mode adds fontification and commands for changing the
|
||||
appearance of backtrace frames. See the node "Backtraces" in the Elisp
|
||||
manual for documentation of the new mode and its commands.
|
||||
|
||||
** Edebug
|
||||
|
||||
+++
|
||||
|
@ -475,14 +483,18 @@ using the new variables 'edebug-behavior-alist',
|
|||
'edebug-new-definition-function'. Edebug's behavior can be changed
|
||||
globally or for individual definitions.
|
||||
|
||||
+++
|
||||
*** Edebug's backtrace buffer now uses 'backtrace-mode'.
|
||||
Backtrace mode adds fontification, links and commands for changing the
|
||||
appearance of backtrace frames. See the node "Backtraces" in the Elisp
|
||||
manual for documentation of the new mode and its commands.
|
||||
|
||||
** Enhanced xterm support
|
||||
|
||||
*** New variable 'xterm-set-window-title' controls whether Emacs sets
|
||||
the XTerm window title. This feature is experimental and is disabled
|
||||
by default.
|
||||
|
||||
** Gamegrid
|
||||
|
||||
** grep
|
||||
|
||||
+++
|
||||
|
@ -499,6 +511,14 @@ The abbreviation can be disabled by the new option
|
|||
*** New variable 'ert-quiet' allows to make ERT output in batch mode
|
||||
less verbose by removing non-essential information.
|
||||
|
||||
+++
|
||||
*** ERT's backtrace buffer now uses 'backtrace-mode'.
|
||||
Backtrace mode adds fontification and commands for changing the
|
||||
appearance of backtrace frames. See the node "Backtraces" in the Elisp
|
||||
manual for documentation of the new mode and its commands.
|
||||
|
||||
** Gamegrid
|
||||
|
||||
---
|
||||
*** Gamegrid now determines its default glyph size based on display
|
||||
dimensions, instead of always using 16 pixels. As a result, Tetris,
|
||||
|
@ -669,6 +689,13 @@ transport strategies as well as a separate API to use them. A
|
|||
transport implementation for process-based communication, such as is
|
||||
used by the Language Server Protocol (LSP), is readily available.
|
||||
|
||||
+++
|
||||
** Backtrace mode improves viewing of Elisp backtraces.
|
||||
Backtrace mode adds pretty printing, fontification and ellipsis
|
||||
expansion to backtrace buffers produced by the Lisp debugger, Edebug
|
||||
and ERT. See the node "Backtraces" in the Elisp manual for
|
||||
documentation of the new mode and its commands.
|
||||
|
||||
|
||||
* Incompatible Lisp Changes in Emacs 27.1
|
||||
|
||||
|
|
767
lisp/emacs-lisp/backtrace.el
Normal file
767
lisp/emacs-lisp/backtrace.el
Normal file
|
@ -0,0 +1,767 @@
|
|||
;;; backtrace.el --- generic major mode for Elisp backtraces -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018 Free Software Foundation, Inc.
|
||||
|
||||
;; Author: Gemini Lasswell
|
||||
;; Keywords: lisp, tools, maint
|
||||
;; Version: 1.0
|
||||
|
||||
;; This file is part of GNU Emacs.
|
||||
|
||||
;; GNU Emacs is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; GNU Emacs is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; This file defines Backtrace mode, a generic major mode for displaying
|
||||
;; Elisp stack backtraces, which can be used as is or inherited from
|
||||
;; by another mode.
|
||||
|
||||
;; For usage information, see the documentation of `backtrace-mode'.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(eval-when-compile (require 'cl-lib))
|
||||
(eval-when-compile (require 'pcase))
|
||||
(eval-when-compile (require 'subr-x)) ; if-let
|
||||
(require 'help-mode) ; Define `help-function-def' button type.
|
||||
(require 'lisp-mode)
|
||||
|
||||
;;; Options
|
||||
|
||||
(defgroup backtrace nil
|
||||
"Viewing of Elisp backtraces."
|
||||
:group 'lisp)
|
||||
|
||||
(defcustom backtrace-fontify t
|
||||
"If non-nil, fontify Backtrace buffers.
|
||||
Set to nil to disable fontification, which may be necessary in
|
||||
order to debug the code that does fontification."
|
||||
:type 'boolean
|
||||
:group 'backtrace
|
||||
:version "27.1")
|
||||
|
||||
(defcustom backtrace-line-length 5000
|
||||
"Target length for lines in Backtrace buffers.
|
||||
Backtrace mode will attempt to abbreviate printing of backtrace
|
||||
frames to make them shorter than this, but success is not
|
||||
guaranteed."
|
||||
:type 'integer
|
||||
:group 'backtrace
|
||||
:version "27.1")
|
||||
|
||||
;;; Backtrace frame data structure
|
||||
|
||||
(cl-defstruct
|
||||
(backtrace-frame
|
||||
(:constructor backtrace-make-frame))
|
||||
evald fun args flags locals pos)
|
||||
|
||||
(cl-defun backtrace-get-frames
|
||||
(&optional base &key (constructor #'backtrace-make-frame))
|
||||
"Collect all frames of current backtrace into a list.
|
||||
The list will contain objects made by CONSTRUCTOR, which
|
||||
defaults to `backtrace-make-frame' and which, if provided, should
|
||||
be the constructor of a structure which includes
|
||||
`backtrace-frame'. If non-nil, BASE should be a function, and
|
||||
frames before its nearest activation frame are discarded."
|
||||
(let ((frames nil)
|
||||
(eval-buffers eval-buffer-list))
|
||||
(mapbacktrace (lambda (evald fun args flags)
|
||||
(push (funcall constructor
|
||||
:evald evald :fun fun
|
||||
:args args :flags flags)
|
||||
frames))
|
||||
(or base 'backtrace-get-frames))
|
||||
(setq frames (nreverse frames))
|
||||
;; Add local variables to each frame, and the buffer position
|
||||
;; to frames containing eval-buffer or eval-region.
|
||||
(dotimes (idx (length frames))
|
||||
(let ((frame (nth idx frames)))
|
||||
;; `backtrace--locals' gives an error when idx is 0. But the
|
||||
;; locals for frame 0 are not needed, because when we get here
|
||||
;; from debug-on-entry, the locals aren't bound yet, and when
|
||||
;; coming from Edebug or ERT there is an Edebug or ERT
|
||||
;; function at frame 0.
|
||||
(when (> idx 0)
|
||||
(setf (backtrace-frame-locals frame)
|
||||
(backtrace--locals idx (or base 'backtrace-get-frames))))
|
||||
(when (and eval-buffers (memq (backtrace-frame-fun frame)
|
||||
'(eval-buffer eval-region)))
|
||||
;; This will get the wrong result if there are two nested
|
||||
;; eval-region calls for the same buffer. That's not a very
|
||||
;; useful case.
|
||||
(with-current-buffer (pop eval-buffers)
|
||||
(setf (backtrace-frame-pos frame) (point))))))
|
||||
frames))
|
||||
|
||||
;; Font Locking support
|
||||
|
||||
(defconst backtrace--font-lock-keywords
|
||||
'((backtrace--match-ellipsis-in-string
|
||||
(1 'button prepend)))
|
||||
"Expressions to fontify in Backtrace mode.
|
||||
Fontify these in addition to the expressions Emacs Lisp mode
|
||||
fontifies.")
|
||||
|
||||
(defconst backtrace-font-lock-keywords
|
||||
(append lisp-el-font-lock-keywords-for-backtraces
|
||||
backtrace--font-lock-keywords)
|
||||
"Default expressions to highlight in Backtrace mode.")
|
||||
(defconst backtrace-font-lock-keywords-1
|
||||
(append lisp-el-font-lock-keywords-for-backtraces-1
|
||||
backtrace--font-lock-keywords)
|
||||
"Subdued level highlighting for Backtrace mode.")
|
||||
(defconst backtrace-font-lock-keywords-2
|
||||
(append lisp-el-font-lock-keywords-for-backtraces-2
|
||||
backtrace--font-lock-keywords)
|
||||
"Gaudy level highlighting for Backtrace mode.")
|
||||
|
||||
(defun backtrace--match-ellipsis-in-string (bound)
|
||||
;; Fontify ellipses within strings as buttons.
|
||||
(when (re-search-forward "\\(\\.\\.\\.\\)\"" bound t)
|
||||
(and (get-text-property (- (point) 2) 'cl-print-ellipsis)
|
||||
(get-text-property (- (point) 3) 'cl-print-ellipsis)
|
||||
(get-text-property (- (point) 4) 'cl-print-ellipsis))))
|
||||
|
||||
;;; Xref support
|
||||
|
||||
(defun backtrace--xref-backend () 'elisp)
|
||||
|
||||
;;; Backtrace mode variables
|
||||
|
||||
(defvar-local backtrace-frames nil
|
||||
"Stack frames displayed in the current Backtrace buffer.
|
||||
This should be a list of `backtrace-frame' objects.")
|
||||
|
||||
(defvar-local backtrace-view nil
|
||||
"A plist describing how to render backtrace frames.
|
||||
Possible entries are :show-flags, :do-xrefs and :print-circle.")
|
||||
|
||||
(defvar-local backtrace-insert-header-function nil
|
||||
"Function for inserting a header for the current Backtrace buffer.
|
||||
If nil, no header will be created. Note that Backtrace buffers
|
||||
are fontified as in Emacs Lisp Mode, the header text included.")
|
||||
|
||||
(defvar backtrace-revert-hook nil
|
||||
"Hook run before reverting a Backtrace buffer.
|
||||
This is commonly used to recompute `backtrace-frames'.")
|
||||
|
||||
(defvar-local backtrace-print-function #'cl-prin1
|
||||
"Function used to print values in the current Backtrace buffer.")
|
||||
|
||||
(defvar backtrace-mode-map
|
||||
(let ((map (copy-keymap special-mode-map)))
|
||||
(set-keymap-parent map button-buffer-map)
|
||||
(define-key map "n" 'backtrace-forward-frame)
|
||||
(define-key map "p" 'backtrace-backward-frame)
|
||||
(define-key map "v" 'backtrace-toggle-locals)
|
||||
(define-key map "#" 'backtrace-toggle-print-circle)
|
||||
(define-key map "\C-m" 'backtrace-help-follow-symbol)
|
||||
(define-key map "+" 'backtrace-pretty-print)
|
||||
(define-key map "=" 'backtrace-collapse)
|
||||
(define-key map [follow-link] 'mouse-face)
|
||||
(define-key map [mouse-2] 'mouse-select-window)
|
||||
map)
|
||||
"Local keymap for `backtrace-mode' buffers.")
|
||||
|
||||
;;; Navigation and Text Properties
|
||||
|
||||
;; This mode uses the following text properties:
|
||||
;; backtrace-index: The index into the buffer-local variable
|
||||
;; `backtrace-frames' for the frame at point, or nil if outside of a
|
||||
;; frame (in the buffer header).
|
||||
;; backtrace-view: A plist describing how the frame is printed. See
|
||||
;; the docstring for the buffer-local variable `backtrace-view.
|
||||
;; backtrace-section: The part of a frame which point is in. Either
|
||||
;; `func' or `locals'. At the moment just used to show and hide the
|
||||
;; local variables. Derived modes which do additional printing
|
||||
;; could define their own frame sections.
|
||||
;; backtrace-form: A value applied to each printed representation of a
|
||||
;; top-level s-expression, which needs to be different for sexps
|
||||
;; printed adjacent to each other, so the limits can be quickly
|
||||
;; found for pretty-printing. The value chosen is a list contining
|
||||
;; the values of print-level and print-length used to print the
|
||||
;; sexp, and those values are used when expanding ellipses.
|
||||
|
||||
(defsubst backtrace-get-index (&optional pos)
|
||||
"Return the index of the backtrace frame at POS.
|
||||
The value is an index into `backtrace-frames', or nil.
|
||||
POS, if omitted or nil, defaults to point."
|
||||
(get-text-property (or pos (point)) 'backtrace-index))
|
||||
|
||||
(defsubst backtrace-get-section (&optional pos)
|
||||
"Return the section of a backtrace frame at POS.
|
||||
POS, if omitted or nil, defaults to point."
|
||||
(get-text-property (or pos (point)) 'backtrace-section))
|
||||
|
||||
(defsubst backtrace-get-view (&optional pos)
|
||||
"Return the view plist of the backtrace frame at POS.
|
||||
POS, if omitted or nil, defaults to point."
|
||||
(get-text-property (or pos (point)) 'backtrace-view))
|
||||
|
||||
(defsubst backtrace-get-form (&optional pos)
|
||||
"Return the backtrace form data for the form printed at POS.
|
||||
POS, if omitted or nil, defaults to point."
|
||||
(get-text-property (or pos (point)) 'backtrace-form))
|
||||
|
||||
(defun backtrace-get-frame-start (&optional pos)
|
||||
"Return the beginning position of the frame at POS in the buffer.
|
||||
POS, if omitted or nil, defaults to point."
|
||||
(let ((posn (or pos (point))))
|
||||
(if (or (= (point-min) posn)
|
||||
(not (eq (backtrace-get-index posn)
|
||||
(backtrace-get-index (1- posn)))))
|
||||
posn
|
||||
(previous-single-property-change posn 'backtrace-index nil (point-min)))))
|
||||
|
||||
(defun backtrace-get-frame-end (&optional pos)
|
||||
"Return the position of the end of the frame at POS in the buffer.
|
||||
POS, if omitted or nil, defaults to point."
|
||||
(next-single-property-change (or pos (point))
|
||||
'backtrace-index nil (point-max)))
|
||||
|
||||
(defun backtrace-get-section-end (&optional pos)
|
||||
"Return the position of the end of the frame section at POS.
|
||||
POS, if omitted or nil, defaults to point."
|
||||
(let* ((frame-end (backtrace-get-frame-end pos))
|
||||
(section-end (next-single-property-change
|
||||
(or pos (point)) 'backtrace-section nil frame-end)))
|
||||
(min frame-end section-end)))
|
||||
|
||||
(defun backtrace-forward-frame ()
|
||||
"Move forward to the beginning of the next frame."
|
||||
(interactive)
|
||||
(let ((max (backtrace-get-frame-end)))
|
||||
(when (= max (point-max))
|
||||
(user-error "No next stack frame"))
|
||||
(goto-char max)))
|
||||
|
||||
(defun backtrace-backward-frame ()
|
||||
"Move backward to the start of a stack frame."
|
||||
(interactive)
|
||||
(let ((current-index (backtrace-get-index))
|
||||
(min (backtrace-get-frame-start)))
|
||||
(if (or (and (/= (point) (point-max)) (null current-index))
|
||||
(= min (point-min))
|
||||
(and (= min (point))
|
||||
(null (backtrace-get-index (1- min)))))
|
||||
(user-error "No previous stack frame"))
|
||||
(if (= min (point))
|
||||
(goto-char (backtrace-get-frame-start (1- min)))
|
||||
(goto-char min))))
|
||||
|
||||
;; Other Backtrace mode commands
|
||||
|
||||
(defun backtrace-revert (&rest _ignored)
|
||||
"The `revert-buffer-function' for `backtrace-mode'.
|
||||
It runs `backtrace-revert-hook', then calls `backtrace-print'."
|
||||
(interactive)
|
||||
(unless (derived-mode-p 'backtrace-mode)
|
||||
(error "The current buffer is not in Backtrace mode"))
|
||||
(run-hooks 'backtrace-revert-hook)
|
||||
(backtrace-print t))
|
||||
|
||||
(defun backtrace-toggle-locals ()
|
||||
"Toggle the display of local variables for the backtrace frame at point.
|
||||
TODO with argument, toggle all frames."
|
||||
(interactive)
|
||||
(let ((index (backtrace-get-index)))
|
||||
(unless index
|
||||
(user-error "Not in a stack frame"))
|
||||
(let ((pos (point)))
|
||||
(goto-char (backtrace-get-frame-start))
|
||||
(while (and (eq index (backtrace-get-index))
|
||||
(not (eq (backtrace-get-section) 'locals)))
|
||||
(goto-char (next-single-property-change (point) 'backtrace-section)))
|
||||
(let ((end (backtrace-get-section-end)))
|
||||
(backtrace--set-locals-visible (point) end (invisible-p (point)))
|
||||
|
||||
(goto-char (if (invisible-p pos) end pos))))))
|
||||
|
||||
(defun backtrace--set-locals-visible (beg end visible)
|
||||
(backtrace--change-button-skip beg end (not visible))
|
||||
(if visible
|
||||
(remove-overlays beg end 'invisible t)
|
||||
(let ((o (make-overlay beg end)))
|
||||
(overlay-put o 'invisible t)
|
||||
(overlay-put o 'evaporate t))))
|
||||
|
||||
(defun backtrace--change-button-skip (beg end value)
|
||||
"Change the skip property on all buttons between BEG and END.
|
||||
Set it to VALUE unless the button is a `backtrace-ellipsis' button."
|
||||
(let ((inhibit-read-only t))
|
||||
(setq beg (next-button beg))
|
||||
(while (and beg (< beg end))
|
||||
(unless (eq (button-type beg) 'backtrace-ellipsis)
|
||||
(button-put beg 'skip value))
|
||||
(setq beg (next-button beg)))))
|
||||
|
||||
(defun backtrace-toggle-print-circle ()
|
||||
"Toggle `print-circle' for the backtrace frame at point."
|
||||
;; TODO with argument, toggle the whole buffer.
|
||||
(interactive)
|
||||
(backtrace--toggle-feature :print-circle))
|
||||
|
||||
(defun backtrace--toggle-feature (feature)
|
||||
"Toggle FEATURE for the backtrace frame at point.
|
||||
FEATURE should be one of the options in `backtrace-view'.
|
||||
After toggling the feature, reprint the frame and position
|
||||
point at the start of the section of the frame it was in
|
||||
before."
|
||||
;; TODO preserve (in)visibility of locals
|
||||
(let ((index (backtrace-get-index))
|
||||
(view (copy-sequence (backtrace-get-view))))
|
||||
(unless index
|
||||
(user-error "Not in a stack frame"))
|
||||
(setq view (plist-put view feature (not (plist-get view feature))))
|
||||
(let ((inhibit-read-only t)
|
||||
(index (backtrace-get-index))
|
||||
(section (backtrace-get-section))
|
||||
(min (backtrace-get-frame-start))
|
||||
(max (backtrace-get-frame-end)))
|
||||
(delete-region min max)
|
||||
(goto-char min)
|
||||
(backtrace-print-frame (nth index backtrace-frames) view)
|
||||
(add-text-properties min (point)
|
||||
`(backtrace-index ,index backtrace-view ,view))
|
||||
(goto-char min)
|
||||
(when (not (eq section (backtrace-get-section)))
|
||||
(if-let ((pos (text-property-any (backtrace-get-frame-start)
|
||||
(backtrace-get-frame-end)
|
||||
'backtrace-section section)))
|
||||
(goto-char pos))))))
|
||||
|
||||
(defmacro backtrace--with-output-variables (view &rest body)
|
||||
"Bind output variables according to VIEW and execute BODY."
|
||||
(declare (indent 1))
|
||||
`(let ((print-escape-control-characters t)
|
||||
(print-escape-newlines t)
|
||||
(print-circle (plist-get ,view :print-circle))
|
||||
(standard-output (current-buffer)))
|
||||
,@body))
|
||||
|
||||
(defun backtrace-expand-ellipsis (button)
|
||||
"Expand display of the elided form at BUTTON."
|
||||
;; TODO a command to expand all ... in form at point
|
||||
;; with argument, don't bind print-level, length??
|
||||
;; Enable undo so there's a way to go back?
|
||||
(interactive)
|
||||
(goto-char (button-start button))
|
||||
(unless (get-text-property (point) 'cl-print-ellipsis)
|
||||
(if (and (> (point) (point-min))
|
||||
(get-text-property (1- (point)) 'cl-print-ellipsis))
|
||||
(backward-char)
|
||||
(user-error "No ellipsis to expand here")))
|
||||
(let* ((end (next-single-property-change (point) 'cl-print-ellipsis))
|
||||
(begin (previous-single-property-change end 'cl-print-ellipsis))
|
||||
(value (get-text-property begin 'cl-print-ellipsis))
|
||||
(props (backtrace-get-text-properties begin))
|
||||
(tag (backtrace-get-form begin))
|
||||
(length (nth 0 tag)) ; TODO should this work with a target char count
|
||||
(level (nth 1 tag)) ; like backtrace-print-to-string?
|
||||
(inhibit-read-only t))
|
||||
(backtrace--with-output-variables (backtrace-get-view)
|
||||
(let ((print-level level)
|
||||
(print-length length))
|
||||
(delete-region begin end)
|
||||
(cl-print-expand-ellipsis value (current-buffer))
|
||||
(setq end (point))
|
||||
(goto-char begin)
|
||||
(while (< (point) end)
|
||||
(let ((next (next-single-property-change (point) 'cl-print-ellipsis
|
||||
nil end)))
|
||||
(when (get-text-property (point) 'cl-print-ellipsis)
|
||||
(make-text-button (point) next :type 'backtrace-ellipsis))
|
||||
(goto-char next)))
|
||||
(goto-char begin)
|
||||
(add-text-properties begin end props)))))
|
||||
|
||||
(defun backtrace-pretty-print ()
|
||||
"Pretty-print the top level s-expression at point."
|
||||
(interactive)
|
||||
(backtrace--reformat-sexp #'backtrace--pretty-print
|
||||
"No form here to pretty-print"))
|
||||
|
||||
(defun backtrace--pretty-print ()
|
||||
"Pretty print the current buffer, then remove the trailing newline."
|
||||
(set-syntax-table emacs-lisp-mode-syntax-table)
|
||||
(pp-buffer)
|
||||
(goto-char (1- (point-max)))
|
||||
(delete-char 1))
|
||||
|
||||
(defun backtrace-collapse ()
|
||||
"Collapse the top level s-expression at point onto one line."
|
||||
(interactive)
|
||||
(backtrace--reformat-sexp #'backtrace--collapse "No form here to collapse"))
|
||||
|
||||
(defun backtrace--collapse ()
|
||||
"Replace line breaks and following indentation with spaces.
|
||||
Works on the current buffer."
|
||||
(goto-char (point-min))
|
||||
(while (re-search-forward "\n[[:blank:]]*" nil t)
|
||||
(replace-match " ")))
|
||||
|
||||
(defun backtrace--reformat-sexp (format-function error-message)
|
||||
"Reformat the top level sexp at point.
|
||||
Locate the top level sexp at or following point on the same line,
|
||||
and reformat it with FORMAT-FUNCTION, preserving the location of
|
||||
point within the sexp. If no sexp is found before the end of
|
||||
the line or buffer, show ERROR-MESSAGE instead.
|
||||
|
||||
FORMAT-FUNCTION will be called without arguments, with the
|
||||
current buffer set to a temporary buffer containing only the
|
||||
content of the sexp."
|
||||
(let* ((orig-pos (point))
|
||||
(pos (point))
|
||||
(tag (backtrace-get-form pos))
|
||||
(end (next-single-property-change pos 'backtrace-form))
|
||||
(begin (previous-single-property-change end 'backtrace-form
|
||||
nil (point-min))))
|
||||
(unless tag
|
||||
(when (or (= end (point-max)) (> end (point-at-eol)))
|
||||
(user-error error-message))
|
||||
(goto-char end)
|
||||
(setq pos end
|
||||
end (next-single-property-change pos 'backtrace-form)
|
||||
begin (previous-single-property-change end 'backtrace-form
|
||||
nil (point-min))))
|
||||
(let* ((offset (when (>= orig-pos begin) (- orig-pos begin)))
|
||||
(offset-marker (when offset (make-marker)))
|
||||
(content (buffer-substring begin end))
|
||||
(props (backtrace-get-text-properties begin))
|
||||
(inhibit-read-only t))
|
||||
(delete-region begin end)
|
||||
(insert (with-temp-buffer
|
||||
(insert content)
|
||||
(when offset
|
||||
(set-marker-insertion-type offset-marker t)
|
||||
(set-marker offset-marker (+ (point-min) offset)))
|
||||
(funcall format-function)
|
||||
(when offset
|
||||
(setq offset (- (marker-position offset-marker) (point-min))))
|
||||
(buffer-string)))
|
||||
(when offset
|
||||
(set-marker offset-marker (+ begin offset)))
|
||||
(save-excursion
|
||||
(goto-char begin)
|
||||
(indent-sexp))
|
||||
(add-text-properties begin (point) props)
|
||||
(if offset
|
||||
(goto-char (marker-position offset-marker))
|
||||
(goto-char orig-pos)))))
|
||||
|
||||
(defun backtrace-get-text-properties (pos)
|
||||
"Return a plist of backtrace-mode's text properties at POS."
|
||||
(apply #'append
|
||||
(mapcar (lambda (prop)
|
||||
(list prop (get-text-property pos prop)))
|
||||
'(backtrace-section backtrace-index backtrace-view
|
||||
backtrace-form))))
|
||||
|
||||
(defun backtrace-help-follow-symbol (&optional pos)
|
||||
"Follow cross-reference at POS, defaulting to point.
|
||||
For the cross-reference format, see `help-make-xrefs'."
|
||||
(interactive "d")
|
||||
(unless pos
|
||||
(setq pos (point)))
|
||||
(unless (push-button pos)
|
||||
;; Check if the symbol under point is a function or variable.
|
||||
(let ((sym
|
||||
(intern
|
||||
(save-excursion
|
||||
(goto-char pos) (skip-syntax-backward "w_")
|
||||
(buffer-substring (point)
|
||||
(progn (skip-syntax-forward "w_")
|
||||
(point)))))))
|
||||
(when (or (boundp sym) (fboundp sym) (facep sym))
|
||||
(describe-symbol sym)))))
|
||||
|
||||
;; Print backtrace frames
|
||||
|
||||
(defun backtrace-print (&optional remember-pos)
|
||||
"Populate the current Backtrace mode buffer.
|
||||
This erases the buffer and inserts printed representations of the
|
||||
frames. Optional argument REMEMBER-POS, if non-nil, means to
|
||||
move point to the entry with the same ID element as the current
|
||||
line and recenter window line accordingly."
|
||||
(let ((inhibit-read-only t)
|
||||
entry-index saved-pt window-line)
|
||||
(and remember-pos
|
||||
(setq entry-index (backtrace-get-index))
|
||||
(when (eq (window-buffer) (current-buffer))
|
||||
(setq window-line
|
||||
(count-screen-lines (window-start) (point)))))
|
||||
(erase-buffer)
|
||||
(when backtrace-insert-header-function
|
||||
(funcall backtrace-insert-header-function))
|
||||
(dotimes (idx (length backtrace-frames))
|
||||
(let ((beg (point))
|
||||
(elt (nth idx backtrace-frames)))
|
||||
(and entry-index
|
||||
(equal entry-index idx)
|
||||
(setq entry-index nil
|
||||
saved-pt (point)))
|
||||
(backtrace-print-frame elt backtrace-view)
|
||||
(add-text-properties
|
||||
beg (point)
|
||||
`(backtrace-index ,idx backtrace-view ,backtrace-view))))
|
||||
(set-buffer-modified-p nil)
|
||||
;; If REMEMBER-POS was specified, move to the "old" location.
|
||||
(if saved-pt
|
||||
(progn (goto-char saved-pt)
|
||||
(when window-line
|
||||
(recenter window-line)))
|
||||
(goto-char (point-min)))))
|
||||
|
||||
;; Define button type used for ...'s.
|
||||
;; Set skip property so you don't have to TAB through 100 of them to
|
||||
;; get to the next function name.
|
||||
(define-button-type 'backtrace-ellipsis
|
||||
'skip t 'action #'backtrace-expand-ellipsis
|
||||
'help-echo "mouse-2, RET: expand this ellipsis")
|
||||
|
||||
(defun backtrace-print-to-string (obj &optional limit)
|
||||
"Return a printed representation of OBJ formatted for backtraces.
|
||||
Attempt to get the length of the returned string under LIMIT
|
||||
charcters with appropriate settings of `print-level' and
|
||||
`print-length.' Attach the settings used with the text property
|
||||
`backtrace-form'. LIMIT defaults to `backtrace-line-length'."
|
||||
(backtrace--with-output-variables backtrace-view
|
||||
(backtrace--print-to-string obj limit)))
|
||||
|
||||
(defun backtrace--print-to-string (sexp &optional limit)
|
||||
;; This is for use by callers who wrap the call with
|
||||
;; backtrace--with-output-variables.
|
||||
(setq limit (or limit backtrace-line-length))
|
||||
(let* ((length 50) ; (/ backtrace-line-length 100) ??
|
||||
(level (truncate (log limit)))
|
||||
(delta (truncate (/ length level))))
|
||||
(with-temp-buffer
|
||||
(catch 'done
|
||||
(while t
|
||||
(erase-buffer)
|
||||
(let ((standard-output (current-buffer))
|
||||
(print-length length)
|
||||
(print-level level))
|
||||
(backtrace--print sexp))
|
||||
;; Stop when either the level is too low or the sexp is
|
||||
;; successfully printed in the space allowed.
|
||||
(when (or (< (- (point-max) (point-min)) limit) (= level 2))
|
||||
(throw 'done nil))
|
||||
(cl-decf level)
|
||||
(cl-decf length delta)))
|
||||
(put-text-property (point-min) (point)
|
||||
'backtrace-form (list length level))
|
||||
;; Make buttons from all the "..."s.
|
||||
;; TODO should this be under control of :do-ellipses in the view
|
||||
;; plist?
|
||||
(goto-char (point-min))
|
||||
(while (< (point) (point-max))
|
||||
(let ((end (next-single-property-change (point) 'cl-print-ellipsis
|
||||
nil (point-max))))
|
||||
(when (get-text-property (point) 'cl-print-ellipsis)
|
||||
(make-text-button (point) end :type 'backtrace-ellipsis))
|
||||
(goto-char end)))
|
||||
(buffer-string))))
|
||||
|
||||
(defun backtrace-print-frame (frame view)
|
||||
"Insert a backtrace FRAME at point formatted according to VIEW.
|
||||
Tag the sections of the frame with the `backtrace-section' text
|
||||
property for use by navigation."
|
||||
(backtrace--with-output-variables view
|
||||
(backtrace--print-flags frame view)
|
||||
(backtrace--print-func-and-args frame view)
|
||||
(backtrace--print-locals frame view)))
|
||||
|
||||
(defun backtrace--print-flags (frame view)
|
||||
"Print the flags of a backtrace FRAME if enabled in VIEW."
|
||||
(let ((beg (point))
|
||||
(flag (plist-get (backtrace-frame-flags frame) :debug-on-exit)))
|
||||
(insert (if (and (plist-get view :show-flags) flag) "* " " "))
|
||||
(put-text-property beg (point) 'backtrace-section 'func)))
|
||||
|
||||
(defun backtrace--print-func-and-args (frame view)
|
||||
"Print the function, arguments and buffer position of a backtrace FRAME.
|
||||
Format it according to VIEW."
|
||||
(let* ((beg (point))
|
||||
(evald (backtrace-frame-evald frame))
|
||||
(fun (backtrace-frame-fun frame))
|
||||
(args (backtrace-frame-args frame))
|
||||
(fun-file (and (plist-get view :do-xrefs) (symbol-file fun 'defun)))
|
||||
(fun-pt (point)))
|
||||
(cond
|
||||
((and evald (not debugger-stack-frame-as-list))
|
||||
(if (atom fun)
|
||||
(funcall backtrace-print-function fun)
|
||||
(insert
|
||||
(backtrace--print-to-string fun (when args (/ backtrace-line-length 2)))))
|
||||
(if args
|
||||
(insert (backtrace--print-to-string
|
||||
args (max (truncate (/ backtrace-line-length 5))
|
||||
(- backtrace-line-length (- (point) beg)))))
|
||||
;; The backtrace-form property is so that
|
||||
;; backtrace-pretty-print will find it.
|
||||
;; backtrace-pretty-print doesn't do anything useful with it,
|
||||
;; just being consistent.
|
||||
(let ((start (point)))
|
||||
(insert "()")
|
||||
(put-text-property start (point) 'backtrace-form t))))
|
||||
(t
|
||||
(let ((fun-and-args (cons fun args)))
|
||||
(insert (backtrace--print-to-string fun-and-args)))
|
||||
(cl-incf fun-pt)))
|
||||
(when fun-file
|
||||
(make-text-button fun-pt (+ fun-pt (length (symbol-name fun)))
|
||||
:type 'help-function-def
|
||||
'help-args (list fun fun-file)))
|
||||
;; After any frame that uses eval-buffer, insert a comment that
|
||||
;; states the buffer position it's reading at.
|
||||
(when (backtrace-frame-pos frame)
|
||||
(insert (format " ; Reading at buffer position %d"
|
||||
(backtrace-frame-pos frame))))
|
||||
(insert "\n")
|
||||
(put-text-property beg (point) 'backtrace-section 'func)))
|
||||
|
||||
(defun backtrace--print-locals (frame _view)
|
||||
"Print a backtrace FRAME's local variables.
|
||||
Make them invisible initially."
|
||||
(let* ((beg (point))
|
||||
(locals (backtrace-frame-locals frame)))
|
||||
(if (null locals)
|
||||
(insert " [no locals]\n")
|
||||
(pcase-dolist (`(,symbol . ,value) locals)
|
||||
(insert " ")
|
||||
(backtrace--print symbol)
|
||||
(insert " = ")
|
||||
(insert (backtrace--print-to-string value))
|
||||
(insert "\n")))
|
||||
(put-text-property beg (point) 'backtrace-section 'locals)
|
||||
(backtrace--set-locals-visible beg (point) nil)))
|
||||
|
||||
(defun backtrace--print (obj)
|
||||
"Attempt to print OBJ using `backtrace-print-function'.
|
||||
Fall back to `prin1' if there is an error."
|
||||
(condition-case err
|
||||
(funcall backtrace-print-function obj)
|
||||
(error
|
||||
(message "Error in backtrace printer: %S" err)
|
||||
(prin1 obj))))
|
||||
|
||||
(defun backtrace-update-flags ()
|
||||
"Update the display of the flags in the backtrace frame at point."
|
||||
(let ((view (backtrace-get-view))
|
||||
(begin (backtrace-get-frame-start)))
|
||||
(when (plist-get view :show-flags)
|
||||
(save-excursion
|
||||
(goto-char begin)
|
||||
(let ((props (backtrace-get-text-properties begin))
|
||||
(inhibit-read-only t)
|
||||
(standard-output (current-buffer)))
|
||||
(delete-char 2)
|
||||
(backtrace--print-flags (nth (backtrace-get-index) backtrace-frames)
|
||||
view)
|
||||
(add-text-properties begin (point) props))))))
|
||||
|
||||
(defun backtrace--filter-visible (beg end &optional _delete)
|
||||
"Return the visible text between BEG and END."
|
||||
(let ((result ""))
|
||||
(while (< beg end)
|
||||
(let ((next (next-single-char-property-change beg 'invisible)))
|
||||
(unless (get-char-property beg 'invisible)
|
||||
(setq result (concat result (buffer-substring beg (min end next)))))
|
||||
(setq beg next)))
|
||||
result))
|
||||
|
||||
;;; The mode definition
|
||||
|
||||
(define-derived-mode backtrace-mode special-mode "Backtrace"
|
||||
"Generic major mode for examining an Elisp stack backtrace.
|
||||
This mode can be used directly, or other major modes can be
|
||||
derived from it, using `define-derived-mode'.
|
||||
|
||||
In this major mode, the buffer contains some optional lines of
|
||||
header text followed by backtrace frames, each consisting of one
|
||||
or more whole lines.
|
||||
|
||||
Letters in this mode do not insert themselves; instead they are
|
||||
commands.
|
||||
\\<backtrace-mode-map>
|
||||
\\{backtrace-mode-map}
|
||||
|
||||
A mode which inherits from Backtrace mode, or a command which
|
||||
creates a backtrace-mode buffer, should usually do the following:
|
||||
|
||||
- Set `backtrace-revert-hook', if the buffer contents need
|
||||
to be specially recomputed prior to `revert-buffer'.
|
||||
- Maybe set `backtrace-insert-header-function' to a function to create
|
||||
header text for the buffer.
|
||||
- Set `backtrace-frames' (see below).
|
||||
- Set `backtrace-view' if desired (see below).
|
||||
- Maybe set `backtrace-print-function'.
|
||||
|
||||
A command which creates or switches to a Backtrace mode buffer,
|
||||
such as `ert-results-pop-to-backtrace-for-test-at-point', should
|
||||
initialize `backtrace-frames' to a list of `backtrace-frame'
|
||||
objects (`backtrace-get-frames' is provided for that purpose, if
|
||||
desired), and `backtrace-view' to a plist describing how it wants
|
||||
the backtrace to appear. Finally, it should call `backtrace-print'.
|
||||
|
||||
`backtrace-print' calls `backtrace-insert-header-function'
|
||||
followed by `backtrace-print-frame', once for each stack frame."
|
||||
:syntax-table emacs-lisp-mode-syntax-table
|
||||
(when backtrace-fontify
|
||||
(setq font-lock-defaults
|
||||
'((backtrace-font-lock-keywords
|
||||
backtrace-font-lock-keywords-1
|
||||
backtrace-font-lock-keywords-2)
|
||||
nil nil nil nil
|
||||
;; TODO This one doesn't look necessary:
|
||||
;; (font-lock-mark-block-function . mark-defun)
|
||||
(font-lock-syntactic-face-function
|
||||
. lisp-font-lock-syntactic-face-function))))
|
||||
(setq truncate-lines t)
|
||||
(buffer-disable-undo)
|
||||
;; In debug.el, from 1998 to 2009 this was set to nil, reason stated
|
||||
;; was because of bytecode. Since 2009 it's been set to t, but the
|
||||
;; default is t so I think this isn't necessary.
|
||||
;; (set-buffer-multibyte t)
|
||||
(setq-local revert-buffer-function #'backtrace-revert)
|
||||
(setq-local filter-buffer-substring-function #'backtrace--filter-visible)
|
||||
(add-hook 'xref-backend-functions #'backtrace--xref-backend nil t))
|
||||
|
||||
(put 'backtrace-mode 'mode-class 'special)
|
||||
|
||||
;;; Backtrace printing
|
||||
|
||||
(defun backtrace-backtrace ()
|
||||
"Print a trace of Lisp function calls currently active.
|
||||
Output stream used is value of `standard-output'."
|
||||
(princ (backtrace-to-string (backtrace-get-frames 'backtrace-backtrace))))
|
||||
|
||||
(defun backtrace-to-string(frames)
|
||||
"Format FRAMES, a list of `backtrace-frame' objects, for output.
|
||||
Return the result as a string."
|
||||
(let ((backtrace-fontify nil))
|
||||
(with-temp-buffer
|
||||
(backtrace-mode)
|
||||
(setq backtrace-view '(:show-flags t)
|
||||
backtrace-frames frames
|
||||
backtrace-print-function #'cl-prin1)
|
||||
(backtrace-print)
|
||||
(substring-no-properties (filter-buffer-substring (point-min)
|
||||
(point-max))))))
|
||||
|
||||
(provide 'backtrace)
|
||||
|
||||
;;; backtrace.el ends here
|
|
@ -28,6 +28,7 @@
|
|||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'backtrace)
|
||||
(require 'button)
|
||||
|
||||
(defgroup debugger nil
|
||||
|
@ -133,6 +134,25 @@ where CAUSE can be:
|
|||
- exit: called because of exit of a flagged function.
|
||||
- error: called because of `debug-on-error'.")
|
||||
|
||||
(cl-defstruct (debugger--buffer-state
|
||||
(:constructor debugger--save-buffer-state
|
||||
(&aux (mode major-mode)
|
||||
(header backtrace-insert-header-function)
|
||||
(frames backtrace-frames)
|
||||
(content (buffer-string))
|
||||
(pos (point)))))
|
||||
mode header frames content pos)
|
||||
|
||||
(defun debugger--restore-buffer-state (state)
|
||||
(unless (derived-mode-p (debugger--buffer-state-mode state))
|
||||
(funcall (debugger--buffer-state-mode state)))
|
||||
(setq backtrace-insert-header-function (debugger--buffer-state-header state)
|
||||
backtrace-frames (debugger--buffer-state-frames state))
|
||||
(let ((inhibit-read-only t))
|
||||
(erase-buffer)
|
||||
(insert (debugger--buffer-state-content state)))
|
||||
(goto-char (debugger--buffer-state-pos state)))
|
||||
|
||||
;;;###autoload
|
||||
(setq debugger 'debug)
|
||||
;;;###autoload
|
||||
|
@ -174,7 +194,7 @@ first will be printed into the backtrace buffer."
|
|||
(debugger-previous-state
|
||||
(if (get-buffer "*Backtrace*")
|
||||
(with-current-buffer (get-buffer "*Backtrace*")
|
||||
(list major-mode (buffer-string)))))
|
||||
(debugger--save-buffer-state))))
|
||||
(debugger-args args)
|
||||
(debugger-buffer (get-buffer-create "*Backtrace*"))
|
||||
(debugger-old-buffer (current-buffer))
|
||||
|
@ -236,7 +256,8 @@ first will be printed into the backtrace buffer."
|
|||
(window-total-height debugger-window)))
|
||||
(error nil)))
|
||||
(setq debugger-previous-window debugger-window))
|
||||
(debugger-mode)
|
||||
(unless (derived-mode-p 'debugger-mode)
|
||||
(debugger-mode))
|
||||
(debugger-setup-buffer debugger-args)
|
||||
(when noninteractive
|
||||
;; If the backtrace is long, save the beginning
|
||||
|
@ -280,15 +301,14 @@ first will be printed into the backtrace buffer."
|
|||
(setq debugger-previous-window nil))
|
||||
;; Restore previous state of debugger-buffer in case we were
|
||||
;; in a recursive invocation of the debugger, otherwise just
|
||||
;; erase the buffer and put it into fundamental mode.
|
||||
;; erase the buffer.
|
||||
(when (buffer-live-p debugger-buffer)
|
||||
(with-current-buffer debugger-buffer
|
||||
(let ((inhibit-read-only t))
|
||||
(erase-buffer)
|
||||
(if (null debugger-previous-state)
|
||||
(fundamental-mode)
|
||||
(insert (nth 1 debugger-previous-state))
|
||||
(funcall (nth 0 debugger-previous-state))))))
|
||||
(if debugger-previous-state
|
||||
(debugger--restore-buffer-state debugger-previous-state)
|
||||
(setq backtrace-insert-header-function nil)
|
||||
(setq backtrace-frames nil)
|
||||
(backtrace-print))))
|
||||
(with-timeout-unsuspend debugger-with-timeout-suspend)
|
||||
(set-match-data debugger-outer-match-data)))
|
||||
(setq debug-on-next-call debugger-step-after-exit)
|
||||
|
@ -301,112 +321,80 @@ first will be printed into the backtrace buffer."
|
|||
(message "Error in debug printer: %S" err)
|
||||
(prin1 obj stream))))
|
||||
|
||||
(defun debugger-insert-backtrace (frames do-xrefs)
|
||||
"Format and insert the backtrace FRAMES at point.
|
||||
Make functions into cross-reference buttons if DO-XREFS is non-nil."
|
||||
(let ((standard-output (current-buffer))
|
||||
(eval-buffers eval-buffer-list))
|
||||
(require 'help-mode) ; Define `help-function-def' button type.
|
||||
(pcase-dolist (`(,evald ,fun ,args ,flags) frames)
|
||||
(insert (if (plist-get flags :debug-on-exit)
|
||||
"* " " "))
|
||||
(let ((fun-file (and do-xrefs (symbol-file fun 'defun)))
|
||||
(fun-pt (point)))
|
||||
(cond
|
||||
((and evald (not debugger-stack-frame-as-list))
|
||||
(debugger--print fun)
|
||||
(if args (debugger--print args) (princ "()")))
|
||||
(t
|
||||
(debugger--print (cons fun args))
|
||||
(cl-incf fun-pt)))
|
||||
(when fun-file
|
||||
(make-text-button fun-pt (+ fun-pt (length (symbol-name fun)))
|
||||
:type 'help-function-def
|
||||
'help-args (list fun fun-file))))
|
||||
;; After any frame that uses eval-buffer, insert a line that
|
||||
;; states the buffer position it's reading at.
|
||||
(when (and eval-buffers (memq fun '(eval-buffer eval-region)))
|
||||
(insert (format " ; Reading at buffer position %d"
|
||||
;; This will get the wrong result if there are
|
||||
;; two nested eval-region calls for the same
|
||||
;; buffer. That's not a very useful case.
|
||||
(with-current-buffer (pop eval-buffers)
|
||||
(point)))))
|
||||
(insert "\n"))))
|
||||
|
||||
(defun debugger-setup-buffer (args)
|
||||
"Initialize the `*Backtrace*' buffer for entry to the debugger.
|
||||
That buffer should be current already."
|
||||
(setq buffer-read-only nil)
|
||||
(erase-buffer)
|
||||
(set-buffer-multibyte t) ;Why was it nil ? -stef
|
||||
(setq buffer-undo-list t)
|
||||
That buffer should be current already and in debugger-mode."
|
||||
(setq backtrace-frames (nthcdr
|
||||
;; Remove debug--implement-debug-on-entry and the
|
||||
;; advice's `apply' frame.
|
||||
(if (eq (car args) 'debug) 3 1)
|
||||
(backtrace-get-frames 'debug)))
|
||||
(when (eq (car-safe args) 'exit)
|
||||
(setq debugger-value (nth 1 args))
|
||||
(setf (cl-getf (backtrace-frame-flags (car backtrace-frames))
|
||||
:debug-on-exit)
|
||||
nil))
|
||||
|
||||
(setq backtrace-view '(:do-xrefs t :show-flags t)
|
||||
backtrace-insert-header-function (lambda ()
|
||||
(debugger--insert-header args))
|
||||
backtrace-print-function debugger-print-function)
|
||||
(backtrace-print)
|
||||
;; Place point on "stack frame 0" (bug#15101).
|
||||
(goto-char (point-min))
|
||||
(search-forward ":" (line-end-position) t)
|
||||
(when (and (< (point) (line-end-position))
|
||||
(= (char-after) ?\s))
|
||||
(forward-char)))
|
||||
|
||||
(defun debugger--insert-header (args)
|
||||
"Insert the header for the debugger's Backtrace buffer.
|
||||
Include the reason for debugger entry from ARGS."
|
||||
(insert "Debugger entered")
|
||||
(let ((frames (nthcdr
|
||||
;; Remove debug--implement-debug-on-entry and the
|
||||
;; advice's `apply' frame.
|
||||
(if (eq (car args) 'debug) 3 1)
|
||||
(backtrace-frames 'debug)))
|
||||
(print-escape-newlines t)
|
||||
(print-escape-control-characters t)
|
||||
;; If you increase print-level, add more depth in call_debugger.
|
||||
(print-level 8)
|
||||
(print-length 50)
|
||||
(pos (point)))
|
||||
(pcase (car args)
|
||||
;; lambda is for debug-on-call when a function call is next.
|
||||
;; debug is for debug-on-entry function called.
|
||||
((or `lambda `debug)
|
||||
(insert "--entering a function:\n")
|
||||
(setq pos (1- (point))))
|
||||
;; Exiting a function.
|
||||
(`exit
|
||||
(insert "--returning value: ")
|
||||
(setq pos (point))
|
||||
(setq debugger-value (nth 1 args))
|
||||
(debugger--print debugger-value (current-buffer))
|
||||
(setf (cl-getf (nth 3 (car frames)) :debug-on-exit) nil)
|
||||
(insert ?\n))
|
||||
;; Watchpoint triggered.
|
||||
((and `watchpoint (let `(,symbol ,newval . ,details) (cdr args)))
|
||||
(insert
|
||||
"--"
|
||||
(pcase details
|
||||
(`(makunbound nil) (format "making %s void" symbol))
|
||||
(`(makunbound ,buffer) (format "killing local value of %s in buffer %s"
|
||||
symbol buffer))
|
||||
(`(defvaralias ,_) (format "aliasing %s to %s" symbol newval))
|
||||
(`(let ,_) (format "let-binding %s to %S" symbol newval))
|
||||
(`(unlet ,_) (format "ending let-binding of %s" symbol))
|
||||
(`(set nil) (format "setting %s to %S" symbol newval))
|
||||
(`(set ,buffer) (format "setting %s in buffer %s to %S"
|
||||
symbol buffer newval))
|
||||
(_ (error "unrecognized watchpoint triggered %S" (cdr args))))
|
||||
": ")
|
||||
(setq pos (point))
|
||||
(insert ?\n))
|
||||
;; Debugger entered for an error.
|
||||
(`error
|
||||
(insert "--Lisp error: ")
|
||||
(setq pos (point))
|
||||
(debugger--print (nth 1 args) (current-buffer))
|
||||
(insert ?\n))
|
||||
;; debug-on-call, when the next thing is an eval.
|
||||
(`t
|
||||
(insert "--beginning evaluation of function call form:\n")
|
||||
(setq pos (1- (point))))
|
||||
;; User calls debug directly.
|
||||
(_
|
||||
(insert ": ")
|
||||
(setq pos (point))
|
||||
(debugger--print
|
||||
(if (eq (car args) 'nil)
|
||||
(cdr args) args)
|
||||
(current-buffer))
|
||||
(insert ?\n)))
|
||||
(debugger-insert-backtrace frames t)
|
||||
;; Place point on "stack frame 0" (bug#15101).
|
||||
(goto-char pos)))
|
||||
(pcase (car args)
|
||||
;; lambda is for debug-on-call when a function call is next.
|
||||
;; debug is for debug-on-entry function called.
|
||||
((or `lambda `debug)
|
||||
(insert "--entering a function:\n"))
|
||||
;; Exiting a function.
|
||||
(`exit
|
||||
(insert "--returning value: ")
|
||||
(insert (backtrace-print-to-string debugger-value))
|
||||
(insert ?\n))
|
||||
;; Watchpoint triggered.
|
||||
((and `watchpoint (let `(,symbol ,newval . ,details) (cdr args)))
|
||||
(insert
|
||||
"--"
|
||||
(pcase details
|
||||
(`(makunbound nil) (format "making %s void" symbol))
|
||||
(`(makunbound ,buffer) (format "killing local value of %s in buffer %s"
|
||||
symbol buffer))
|
||||
(`(defvaralias ,_) (format "aliasing %s to %s" symbol newval))
|
||||
(`(let ,_) (format "let-binding %s to %s" symbol
|
||||
(backtrace-print-to-string newval)))
|
||||
(`(unlet ,_) (format "ending let-binding of %s" symbol))
|
||||
(`(set nil) (format "setting %s to %s" symbol
|
||||
(backtrace-print-to-string newval)))
|
||||
(`(set ,buffer) (format "setting %s in buffer %s to %s"
|
||||
symbol buffer
|
||||
(backtrace-print-to-string newval)))
|
||||
(_ (error "unrecognized watchpoint triggered %S" (cdr args))))
|
||||
": ")
|
||||
(insert ?\n))
|
||||
;; Debugger entered for an error.
|
||||
(`error
|
||||
(insert "--Lisp error: ")
|
||||
(insert (backtrace-print-to-string (nth 1 args)))
|
||||
(insert ?\n))
|
||||
;; debug-on-call, when the next thing is an eval.
|
||||
(`t
|
||||
(insert "--beginning evaluation of function call form:\n"))
|
||||
;; User calls debug directly.
|
||||
(_
|
||||
(insert ": ")
|
||||
(insert (backtrace-print-to-string (if (eq (car args) 'nil)
|
||||
(cdr args) args)))
|
||||
(insert ?\n))))
|
||||
|
||||
|
||||
(defun debugger-step-through ()
|
||||
|
@ -426,12 +414,12 @@ Enter another debugger on next entry to eval, apply or funcall."
|
|||
(unless debugger-may-continue
|
||||
(error "Cannot continue"))
|
||||
(message "Continuing.")
|
||||
(save-excursion
|
||||
;; Check to see if we've flagged some frame for debug-on-exit, in which
|
||||
;; case we'll probably come back to the debugger soon.
|
||||
(goto-char (point-min))
|
||||
(if (re-search-forward "^\\* " nil t)
|
||||
(setq debugger-will-be-back t)))
|
||||
|
||||
;; Check to see if we've flagged some frame for debug-on-exit, in which
|
||||
;; case we'll probably come back to the debugger soon.
|
||||
(dolist (frame backtrace-frames)
|
||||
(when (plist-get (backtrace-frame-flags frame) :debug-on-exit)
|
||||
(setq debugger-will-be-back t)))
|
||||
(exit-recursive-edit))
|
||||
|
||||
(defun debugger-return-value (val)
|
||||
|
@ -446,12 +434,11 @@ will be used, such as in a debug on exit from a frame."
|
|||
(setq debugger-value val)
|
||||
(princ "Returning " t)
|
||||
(debugger--print debugger-value)
|
||||
(save-excursion
|
||||
;; Check to see if we've flagged some frame for debug-on-exit, in which
|
||||
;; case we'll probably come back to the debugger soon.
|
||||
(goto-char (point-min))
|
||||
(if (re-search-forward "^\\* " nil t)
|
||||
(setq debugger-will-be-back t)))
|
||||
(dolist (frame backtrace-frames)
|
||||
(when (plist-get (backtrace-frame-flags frame) :debug-on-exit)
|
||||
(setq debugger-will-be-back t)))
|
||||
(exit-recursive-edit))
|
||||
|
||||
(defun debugger-jump ()
|
||||
|
@ -473,63 +460,40 @@ removes itself from that hook."
|
|||
|
||||
(defun debugger-frame-number (&optional skip-base)
|
||||
"Return number of frames in backtrace before the one point points at."
|
||||
(save-excursion
|
||||
(beginning-of-line)
|
||||
(if (looking-at " *;;;\\|[a-z]")
|
||||
(error "This line is not a function call"))
|
||||
(let ((opoint (point))
|
||||
(count 0))
|
||||
(unless skip-base
|
||||
(let ((index (backtrace-get-index))
|
||||
(count 0))
|
||||
(unless index
|
||||
(error "This line is not a function call"))
|
||||
(unless skip-base
|
||||
(while (not (eq (cadr (backtrace-frame count)) 'debug))
|
||||
(setq count (1+ count)))
|
||||
;; Skip debug--implement-debug-on-entry frame.
|
||||
(when (eq 'debug--implement-debug-on-entry
|
||||
(cadr (backtrace-frame (1+ count))))
|
||||
(setq count (+ 2 count))))
|
||||
(goto-char (point-min))
|
||||
(when (looking-at "Debugger entered--\\(Lisp error\\|returning value\\):")
|
||||
(goto-char (match-end 0))
|
||||
(forward-sexp 1))
|
||||
(forward-line 1)
|
||||
(while (progn
|
||||
(forward-char 2)
|
||||
(cond ((debugger--locals-visible-p)
|
||||
(goto-char (next-single-char-property-change
|
||||
(point) 'locals-visible)))
|
||||
((= (following-char) ?\()
|
||||
(forward-sexp 1))
|
||||
(t
|
||||
(forward-sexp 2)))
|
||||
(forward-line 1)
|
||||
(<= (point) opoint))
|
||||
(if (looking-at " *;;;")
|
||||
(forward-line 1))
|
||||
(setq count (1+ count)))
|
||||
count)))
|
||||
(+ count index)))
|
||||
|
||||
(defun debugger-frame ()
|
||||
"Request entry to debugger when this frame exits.
|
||||
Applies to the frame whose line point is on in the backtrace."
|
||||
(interactive)
|
||||
(backtrace-debug (debugger-frame-number) t)
|
||||
(beginning-of-line)
|
||||
(if (= (following-char) ? )
|
||||
(let ((inhibit-read-only t))
|
||||
(delete-char 1)
|
||||
(insert ?*)))
|
||||
(beginning-of-line))
|
||||
(setf
|
||||
(cl-getf (backtrace-frame-flags (nth (backtrace-get-index) backtrace-frames))
|
||||
:debug-on-exit)
|
||||
t)
|
||||
(backtrace-update-flags))
|
||||
|
||||
(defun debugger-frame-clear ()
|
||||
"Do not enter debugger when this frame exits.
|
||||
Applies to the frame whose line point is on in the backtrace."
|
||||
(interactive)
|
||||
(backtrace-debug (debugger-frame-number) nil)
|
||||
(beginning-of-line)
|
||||
(if (= (following-char) ?*)
|
||||
(let ((inhibit-read-only t))
|
||||
(delete-char 1)
|
||||
(insert ? )))
|
||||
(beginning-of-line))
|
||||
(setf
|
||||
(cl-getf (backtrace-frame-flags (nth (backtrace-get-index) backtrace-frames))
|
||||
:debug-on-exit)
|
||||
nil)
|
||||
(backtrace-update-flags))
|
||||
|
||||
(defmacro debugger-env-macro (&rest body)
|
||||
"Run BODY in original environment."
|
||||
|
@ -564,69 +528,11 @@ The environment used is the one when entering the activation frame at point."
|
|||
(let ((str (eval-expression-print-format val)))
|
||||
(if str (princ str t))))))))
|
||||
|
||||
(defun debugger--locals-visible-p ()
|
||||
"Are the local variables of the current stack frame visible?"
|
||||
(save-excursion
|
||||
(move-to-column 2)
|
||||
(get-text-property (point) 'locals-visible)))
|
||||
|
||||
(defun debugger--insert-locals (locals)
|
||||
"Insert the local variables LOCALS at point."
|
||||
(cond ((null locals)
|
||||
(insert "\n [no locals]"))
|
||||
(t
|
||||
(let ((print-escape-newlines t))
|
||||
(dolist (s+v locals)
|
||||
(let ((symbol (car s+v))
|
||||
(value (cdr s+v)))
|
||||
(insert "\n ")
|
||||
(prin1 symbol (current-buffer))
|
||||
(insert " = ")
|
||||
(debugger--print value (current-buffer))))))))
|
||||
|
||||
(defun debugger--show-locals ()
|
||||
"For the frame at point, insert locals and add text properties."
|
||||
(let* ((nframe (1+ (debugger-frame-number 'skip-base)))
|
||||
(base (debugger--backtrace-base))
|
||||
(locals (backtrace--locals nframe base))
|
||||
(inhibit-read-only t))
|
||||
(save-excursion
|
||||
(let ((start (progn
|
||||
(move-to-column 2)
|
||||
(point))))
|
||||
(end-of-line)
|
||||
(debugger--insert-locals locals)
|
||||
(add-text-properties start (point) '(locals-visible t))))))
|
||||
|
||||
(defun debugger--hide-locals ()
|
||||
"Delete local variables and remove the text property."
|
||||
(let* ((col (current-column))
|
||||
(end (progn
|
||||
(move-to-column 2)
|
||||
(next-single-char-property-change (point) 'locals-visible)))
|
||||
(start (previous-single-char-property-change end 'locals-visible))
|
||||
(inhibit-read-only t))
|
||||
(remove-text-properties start end '(locals-visible))
|
||||
(goto-char start)
|
||||
(end-of-line)
|
||||
(delete-region (point) end)
|
||||
(move-to-column col)))
|
||||
|
||||
(defun debugger-toggle-locals ()
|
||||
"Show or hide local variables of the current stack frame."
|
||||
(interactive)
|
||||
(cond ((debugger--locals-visible-p)
|
||||
(debugger--hide-locals))
|
||||
(t
|
||||
(debugger--show-locals))))
|
||||
|
||||
|
||||
(defvar debugger-mode-map
|
||||
(let ((map (make-keymap))
|
||||
(menu-map (make-sparse-keymap)))
|
||||
(set-keymap-parent map button-buffer-map)
|
||||
(suppress-keymap map)
|
||||
(define-key map "-" 'negative-argument)
|
||||
(set-keymap-parent map backtrace-mode-map)
|
||||
(define-key map "b" 'debugger-frame)
|
||||
(define-key map "c" 'debugger-continue)
|
||||
(define-key map "j" 'debugger-jump)
|
||||
|
@ -634,24 +540,20 @@ The environment used is the one when entering the activation frame at point."
|
|||
(define-key map "u" 'debugger-frame-clear)
|
||||
(define-key map "d" 'debugger-step-through)
|
||||
(define-key map "l" 'debugger-list-functions)
|
||||
(define-key map "h" 'describe-mode)
|
||||
(define-key map "q" 'top-level)
|
||||
(define-key map "q" 'debugger-quit)
|
||||
(define-key map "e" 'debugger-eval-expression)
|
||||
(define-key map "v" 'debugger-toggle-locals) ; "v" is for "variables".
|
||||
(define-key map " " 'next-line)
|
||||
(define-key map "R" 'debugger-record-expression)
|
||||
(define-key map "\C-m" 'debug-help-follow)
|
||||
(define-key map [mouse-2] 'push-button)
|
||||
(define-key map [menu-bar debugger] (cons "Debugger" menu-map))
|
||||
(define-key menu-map [deb-top]
|
||||
'(menu-item "Quit" top-level
|
||||
'(menu-item "Quit" debugger-quit
|
||||
:help "Quit debugging and return to top level"))
|
||||
(define-key menu-map [deb-s0] '("--"))
|
||||
(define-key menu-map [deb-descr]
|
||||
'(menu-item "Describe Debugger Mode" describe-mode
|
||||
:help "Display documentation for debugger-mode"))
|
||||
(define-key menu-map [deb-hfol]
|
||||
'(menu-item "Help Follow" debug-help-follow
|
||||
'(menu-item "Help Follow" backtrace-help-follow-symbol
|
||||
:help "Follow cross-reference"))
|
||||
(define-key menu-map [deb-nxt]
|
||||
'(menu-item "Next Line" next-line
|
||||
|
@ -689,8 +591,8 @@ The environment used is the one when entering the activation frame at point."
|
|||
|
||||
(put 'debugger-mode 'mode-class 'special)
|
||||
|
||||
(define-derived-mode debugger-mode fundamental-mode "Debugger"
|
||||
"Mode for backtrace buffers, selected in debugger.
|
||||
(define-derived-mode debugger-mode backtrace-mode "Debugger"
|
||||
"Mode for debugging Emacs Lisp using a backtrace.
|
||||
\\<debugger-mode-map>
|
||||
A line starts with `*' if exiting that frame will call the debugger.
|
||||
Type \\[debugger-frame] or \\[debugger-frame-clear] to set or remove the `*'.
|
||||
|
@ -704,8 +606,6 @@ which functions will enter the debugger when called.
|
|||
|
||||
Complete list of commands:
|
||||
\\{debugger-mode-map}"
|
||||
(setq truncate-lines t)
|
||||
(set-syntax-table emacs-lisp-mode-syntax-table)
|
||||
(add-hook 'kill-buffer-hook
|
||||
(lambda () (if (> (recursion-depth) 0) (top-level)))
|
||||
nil t)
|
||||
|
@ -732,27 +632,6 @@ Complete list of commands:
|
|||
(buffer-substring (line-beginning-position 0)
|
||||
(line-end-position 0)))))
|
||||
|
||||
(defun debug-help-follow (&optional pos)
|
||||
"Follow cross-reference at POS, defaulting to point.
|
||||
|
||||
For the cross-reference format, see `help-make-xrefs'."
|
||||
(interactive "d")
|
||||
;; Ideally we'd just do (call-interactively 'help-follow) except that this
|
||||
;; assumes we're already in a *Help* buffer and reuses it, so it ends up
|
||||
;; incorrectly "reusing" the *Backtrace* buffer to show the help info.
|
||||
(unless pos
|
||||
(setq pos (point)))
|
||||
(unless (push-button pos)
|
||||
;; check if the symbol under point is a function or variable
|
||||
(let ((sym
|
||||
(intern
|
||||
(save-excursion
|
||||
(goto-char pos) (skip-syntax-backward "w_")
|
||||
(buffer-substring (point)
|
||||
(progn (skip-syntax-forward "w_")
|
||||
(point)))))))
|
||||
(when (or (boundp sym) (fboundp sym) (facep sym))
|
||||
(describe-symbol sym)))))
|
||||
|
||||
;; When you change this, you may also need to change the number of
|
||||
;; frames that the debugger skips.
|
||||
|
@ -853,6 +732,13 @@ To specify a nil argument interactively, exit with an empty minibuffer."
|
|||
;;(princ "be set to debug on entry, even if it is in the list.")
|
||||
)))))
|
||||
|
||||
(defun debugger-quit ()
|
||||
"Quit debugging and return to the top level."
|
||||
(interactive)
|
||||
(if (= (recursion-depth) 0)
|
||||
(quit-window)
|
||||
(top-level)))
|
||||
|
||||
(defun debug--implement-debug-watch (symbol newval op where)
|
||||
"Conditionally call the debugger.
|
||||
This function is called when SYMBOL's value is modified."
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
|
||||
;;; Code:
|
||||
|
||||
(require 'backtrace)
|
||||
(require 'macroexp)
|
||||
(require 'cl-lib)
|
||||
(eval-when-compile (require 'pcase))
|
||||
|
@ -206,8 +207,7 @@ Use this with caution since it is not debugged."
|
|||
"Non-nil if Edebug should unwrap results of expressions.
|
||||
That is, Edebug will try to remove its own instrumentation from the result.
|
||||
This is useful when debugging macros where the results of expressions
|
||||
are instrumented expressions. But don't do this when results might be
|
||||
circular or an infinite loop will result."
|
||||
are instrumented expressions."
|
||||
:type 'boolean
|
||||
:group 'edebug)
|
||||
|
||||
|
@ -1265,25 +1265,59 @@ purpose by adding an entry to this alist, and setting
|
|||
(defun edebug-unwrap (sexp)
|
||||
"Return the unwrapped SEXP or return it as is if it is not wrapped.
|
||||
The SEXP might be the result of wrapping a body, which is a list of
|
||||
expressions; a `progn' form will be returned enclosing these forms."
|
||||
(if (consp sexp)
|
||||
(cond
|
||||
((eq 'edebug-after (car sexp))
|
||||
(nth 3 sexp))
|
||||
((eq 'edebug-enter (car sexp))
|
||||
(macroexp-progn (nthcdr 2 (nth 1 (nth 3 sexp)))))
|
||||
(t sexp);; otherwise it is not wrapped, so just return it.
|
||||
)
|
||||
sexp))
|
||||
expressions; a `progn' form will be returned enclosing these forms.
|
||||
Does not unwrap inside vectors, records, structures, or hash tables."
|
||||
(pcase sexp
|
||||
(`(edebug-after ,_before-form ,_after-index ,form)
|
||||
form)
|
||||
(`(lambda ,args (edebug-enter ',_sym ,_arglist
|
||||
(function (lambda nil . ,body))))
|
||||
`(lambda ,args ,@body))
|
||||
(`(closure ,env ,args (edebug-enter ',_sym ,_arglist
|
||||
(function (lambda nil . ,body))))
|
||||
`(closure ,env ,args ,@body))
|
||||
(`(edebug-enter ',_sym ,_args (function (lambda nil . ,body)))
|
||||
(macroexp-progn body))
|
||||
(_ sexp)))
|
||||
|
||||
(defun edebug-unwrap* (sexp)
|
||||
"Return the SEXP recursively unwrapped."
|
||||
(let ((ht (make-hash-table :test 'eq)))
|
||||
(edebug--unwrap1 sexp ht)))
|
||||
|
||||
(defun edebug--unwrap1 (sexp hash-table)
|
||||
"Unwrap SEXP using HASH-TABLE of things already unwrapped.
|
||||
HASH-TABLE contains the results of unwrapping cons cells within
|
||||
SEXP, which are reused to avoid infinite loops when SEXP is or
|
||||
contains a circular object."
|
||||
(let ((new-sexp (edebug-unwrap sexp)))
|
||||
(while (not (eq sexp new-sexp))
|
||||
(setq sexp new-sexp
|
||||
new-sexp (edebug-unwrap sexp)))
|
||||
(if (consp new-sexp)
|
||||
(mapcar #'edebug-unwrap* new-sexp)
|
||||
(let ((result (gethash new-sexp hash-table nil)))
|
||||
(unless result
|
||||
(let ((remainder new-sexp)
|
||||
current)
|
||||
(setq result (cons nil nil)
|
||||
current result)
|
||||
(while
|
||||
(progn
|
||||
(puthash remainder current hash-table)
|
||||
(setf (car current)
|
||||
(edebug--unwrap1 (car remainder) hash-table))
|
||||
(setq remainder (cdr remainder))
|
||||
(cond
|
||||
((atom remainder)
|
||||
(setf (cdr current)
|
||||
(edebug--unwrap1 remainder hash-table))
|
||||
nil)
|
||||
((gethash remainder hash-table nil)
|
||||
(setf (cdr current) (gethash remainder hash-table nil))
|
||||
nil)
|
||||
(t (setq current
|
||||
(setf (cdr current) (cons nil nil)))))))))
|
||||
result)
|
||||
new-sexp)))
|
||||
|
||||
|
||||
|
@ -3916,8 +3950,10 @@ Global commands prefixed by `global-edebug-prefix':
|
|||
;; (setq debugger 'debug) ; use the standard debugger
|
||||
|
||||
;; Note that debug and its utilities must be byte-compiled to work,
|
||||
;; since they depend on the backtrace looking a certain way. But
|
||||
;; edebug is not dependent on this, yet.
|
||||
;; since they depend on the backtrace looking a certain way. Edebug
|
||||
;; will work if not byte-compiled, but it will not be able correctly
|
||||
;; remove its instrumentation from backtraces unless it is
|
||||
;; byte-compiled.
|
||||
|
||||
(defun edebug (&optional arg-mode &rest args)
|
||||
"Replacement for `debug'.
|
||||
|
@ -3947,48 +3983,96 @@ Otherwise call `debug' normally."
|
|||
(apply #'debug arg-mode args)
|
||||
))
|
||||
|
||||
;;; Backtrace buffer
|
||||
|
||||
;; Data structure for backtrace frames with information
|
||||
;; from Edebug instrumentation found in the backtrace.
|
||||
(cl-defstruct
|
||||
(edebug--frame
|
||||
(:constructor edebug--make-frame)
|
||||
(:include backtrace-frame))
|
||||
def-name before-index after-index)
|
||||
|
||||
(defun edebug-backtrace ()
|
||||
"Display a non-working backtrace. Better than nothing..."
|
||||
"Display the current backtrace in a `backtrace-mode' window."
|
||||
(interactive)
|
||||
(if (or (not edebug-backtrace-buffer)
|
||||
(null (buffer-name edebug-backtrace-buffer)))
|
||||
(setq edebug-backtrace-buffer
|
||||
(generate-new-buffer "*Backtrace*"))
|
||||
(generate-new-buffer "*Edebug Backtrace*"))
|
||||
;; Else, could just display edebug-backtrace-buffer.
|
||||
)
|
||||
(with-output-to-temp-buffer (buffer-name edebug-backtrace-buffer)
|
||||
(setq edebug-backtrace-buffer standard-output)
|
||||
(let ((print-escape-newlines t)
|
||||
(print-length 50) ; FIXME cf edebug-safe-prin1-to-string
|
||||
last-ok-point)
|
||||
(backtrace)
|
||||
(with-current-buffer edebug-backtrace-buffer
|
||||
(unless (derived-mode-p 'backtrace-mode)
|
||||
(backtrace-mode))
|
||||
(setq backtrace-frames (edebug--backtrace-frames)
|
||||
backtrace-view '(:do-xrefs t))
|
||||
(backtrace-print)
|
||||
(goto-char (point-min)))))
|
||||
|
||||
;; Clean up the backtrace.
|
||||
;; Not quite right for current edebug scheme.
|
||||
(set-buffer edebug-backtrace-buffer)
|
||||
(setq truncate-lines t)
|
||||
(goto-char (point-min))
|
||||
(setq last-ok-point (point))
|
||||
(if t (progn
|
||||
(defun edebug--backtrace-frames ()
|
||||
"Return backtrace frames with instrumentation removed.
|
||||
Remove frames for Edebug's functions and the lambdas in
|
||||
`edebug-enter' wrappers."
|
||||
(let* ((frames (backtrace-get-frames 'edebug-debugger
|
||||
:constructor #'edebug--make-frame))
|
||||
skip-next-lambda def-name before-index after-index
|
||||
results
|
||||
(index (length frames)))
|
||||
(dolist (frame (reverse frames))
|
||||
(let ((fun (edebug--frame-fun frame))
|
||||
(args (edebug--frame-args frame)))
|
||||
(cl-decf index)
|
||||
(when (edebug--frame-evald frame)
|
||||
(setq before-index nil
|
||||
after-index nil))
|
||||
(pcase fun
|
||||
('edebug-enter
|
||||
(setq skip-next-lambda t
|
||||
def-name (nth 0 args)))
|
||||
('edebug-after
|
||||
(setq before-index (if (consp (nth 0 args))
|
||||
(nth 1 (nth 0 args))
|
||||
(nth 0 args))
|
||||
after-index (nth 1 args)))
|
||||
((pred edebug--symbol-not-prefixed-p)
|
||||
(edebug--unwrap-and-add-info frame def-name before-index after-index)
|
||||
(setf (edebug--frame-def-name frame) (and before-index def-name))
|
||||
(setf (edebug--frame-before-index frame) before-index)
|
||||
(setf (edebug--frame-after-index frame) after-index)
|
||||
(push frame results)
|
||||
(setq before-index nil
|
||||
after-index nil))
|
||||
(`(,(or 'lambda 'closure) . ,_)
|
||||
(unless skip-next-lambda
|
||||
(edebug--unwrap-and-add-info frame def-name before-index after-index)
|
||||
(push frame results))
|
||||
(setq before-index nil
|
||||
after-index nil
|
||||
skip-next-lambda nil)))))
|
||||
results))
|
||||
|
||||
;; Delete interspersed edebug internals.
|
||||
(while (re-search-forward "^ (?edebug" nil t)
|
||||
(beginning-of-line)
|
||||
(cond
|
||||
((looking-at "^ (edebug-after")
|
||||
;; Previous lines may contain code, so just delete this line.
|
||||
(setq last-ok-point (point))
|
||||
(forward-line 1)
|
||||
(delete-region last-ok-point (point)))
|
||||
(defun edebug--symbol-not-prefixed-p (sym)
|
||||
"Return non-nil if SYM is a symbol not prefixed by \"edebug-\"."
|
||||
(and (symbolp sym)
|
||||
(not (string-prefix-p "edebug-" (symbol-name sym)))))
|
||||
|
||||
((looking-at (if debugger-stack-frame-as-list
|
||||
"^ (edebug"
|
||||
"^ edebug"))
|
||||
(forward-line 1)
|
||||
(delete-region last-ok-point (point))
|
||||
)))
|
||||
)))))
|
||||
(defun edebug--unwrap-and-add-info (frame def-name before-index after-index)
|
||||
"Update FRAME with the additional info needed by an edebug--frame.
|
||||
Save DEF-NAME, BEFORE-INDEX and AFTER-INDEX in FRAME. Also
|
||||
remove Edebug's instrumentation from the function and any
|
||||
unevaluated arguments in FRAME."
|
||||
(setf (edebug--frame-def-name frame) (and before-index def-name))
|
||||
(setf (edebug--frame-before-index frame) before-index)
|
||||
(setf (edebug--frame-after-index frame) after-index)
|
||||
(setf (edebug--frame-fun frame) (edebug-unwrap* (edebug--frame-fun frame)))
|
||||
(unless (edebug--frame-evald frame)
|
||||
(let (results)
|
||||
(dolist (arg (edebug--frame-args frame))
|
||||
(push (edebug-unwrap* arg) results))
|
||||
(setf (edebug--frame-args frame) (nreverse results)))))
|
||||
|
||||
|
||||
;;; Trace display
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
(require 'cl-lib)
|
||||
(require 'button)
|
||||
(require 'debug)
|
||||
(require 'backtrace)
|
||||
(require 'easymenu)
|
||||
(require 'ewoc)
|
||||
(require 'find-func)
|
||||
|
@ -677,13 +678,6 @@ and is displayed in front of the value of MESSAGE-FORM."
|
|||
(cl-defstruct (ert-test-aborted-with-non-local-exit
|
||||
(:include ert-test-result)))
|
||||
|
||||
(defun ert--print-backtrace (backtrace do-xrefs)
|
||||
"Format the backtrace BACKTRACE to the current buffer."
|
||||
(let ((print-escape-newlines t)
|
||||
(print-level 8)
|
||||
(print-length 50))
|
||||
(debugger-insert-backtrace backtrace do-xrefs)))
|
||||
|
||||
;; A container for the state of the execution of a single test and
|
||||
;; environment data needed during its execution.
|
||||
(cl-defstruct ert--test-execution-info
|
||||
|
@ -732,7 +726,7 @@ run. ARGS are the arguments to `debugger'."
|
|||
;; use.
|
||||
;;
|
||||
;; Grab the frames above the debugger.
|
||||
(backtrace (cdr (backtrace-frames debugger)))
|
||||
(backtrace (cdr (backtrace-get-frames debugger)))
|
||||
(infos (reverse ert--infos)))
|
||||
(setf (ert--test-execution-info-result info)
|
||||
(cl-ecase type
|
||||
|
@ -1406,9 +1400,8 @@ Returns the stats object."
|
|||
(ert-test-result-with-condition
|
||||
(message "Test %S backtrace:" (ert-test-name test))
|
||||
(with-temp-buffer
|
||||
(ert--print-backtrace
|
||||
(ert-test-result-with-condition-backtrace result)
|
||||
nil)
|
||||
(insert (backtrace-to-string
|
||||
(ert-test-result-with-condition-backtrace result)))
|
||||
(if (not ert-batch-backtrace-right-margin)
|
||||
(message "%s"
|
||||
(buffer-substring-no-properties (point-min)
|
||||
|
@ -2450,20 +2443,21 @@ To be used in the ERT results buffer."
|
|||
(cl-etypecase result
|
||||
(ert-test-passed (error "Test passed, no backtrace available"))
|
||||
(ert-test-result-with-condition
|
||||
(let ((backtrace (ert-test-result-with-condition-backtrace result))
|
||||
(buffer (get-buffer-create "*ERT Backtrace*")))
|
||||
(let ((buffer (get-buffer-create "*ERT Backtrace*")))
|
||||
(pop-to-buffer buffer)
|
||||
(let ((inhibit-read-only t))
|
||||
(buffer-disable-undo)
|
||||
(erase-buffer)
|
||||
(ert-simple-view-mode)
|
||||
(set-buffer-multibyte t) ; mimic debugger-setup-buffer
|
||||
(setq truncate-lines t)
|
||||
(ert--print-backtrace backtrace t)
|
||||
(goto-char (point-min))
|
||||
(insert (substitute-command-keys "Backtrace for test `"))
|
||||
(ert-insert-test-name-button (ert-test-name test))
|
||||
(insert (substitute-command-keys "':\n"))))))))
|
||||
(unless (derived-mode-p 'backtrace-mode)
|
||||
(backtrace-mode))
|
||||
(setq backtrace-insert-header-function
|
||||
(lambda () (ert--insert-backtrace-header (ert-test-name test)))
|
||||
backtrace-frames (ert-test-result-with-condition-backtrace result)
|
||||
backtrace-view '(:do-xrefs t))
|
||||
(backtrace-print)
|
||||
(goto-char (point-min)))))))
|
||||
|
||||
(defun ert--insert-backtrace-header (name)
|
||||
(insert (substitute-command-keys "Backtrace for test `"))
|
||||
(ert-insert-test-name-button name)
|
||||
(insert (substitute-command-keys "':\n")))
|
||||
|
||||
(defun ert-results-pop-to-messages-for-test-at-point ()
|
||||
"Display the part of the *Messages* buffer generated during the test at point.
|
||||
|
|
|
@ -517,6 +517,16 @@ This will generate compile-time constants from BINDINGS."
|
|||
(defvar lisp-cl-font-lock-keywords lisp-cl-font-lock-keywords-1
|
||||
"Default expressions to highlight in Lisp modes.")
|
||||
|
||||
;; Support backtrace mode.
|
||||
(defconst lisp-el-font-lock-keywords-for-backtraces lisp-el-font-lock-keywords
|
||||
"Default highlighting from Emacs Lisp mod used in Backtrace mode.")
|
||||
(defconst lisp-el-font-lock-keywords-for-backtraces-1 lisp-el-font-lock-keywords-1
|
||||
"Subdued highlighting from Emacs Lisp mode used in Backtrace mode.")
|
||||
(defconst lisp-el-font-lock-keywords-for-backtraces-2
|
||||
(remove (assoc 'lisp--match-hidden-arg lisp-el-font-lock-keywords-2)
|
||||
lisp-el-font-lock-keywords-2)
|
||||
"Gaudy highlighting from Emacs Lisp mode used in Backtrace mode.")
|
||||
|
||||
(defun lisp-string-in-doc-position-p (listbeg startpos)
|
||||
"Return true if a doc string may occur at STARTPOS inside a list.
|
||||
LISTBEG is the position of the start of the innermost list
|
||||
|
|
89
test/lisp/emacs-lisp/backtrace-tests.el
Normal file
89
test/lisp/emacs-lisp/backtrace-tests.el
Normal file
|
@ -0,0 +1,89 @@
|
|||
;;; backtrace-tests.el --- Tests for emacs-lisp/backtrace.el -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2018 Free Software Foundation, Inc.
|
||||
|
||||
;; Author: Gemini Lasswell
|
||||
|
||||
;; This file is part of GNU Emacs.
|
||||
|
||||
;; GNU Emacs is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; GNU Emacs is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'backtrace)
|
||||
(require 'ert)
|
||||
(require 'seq)
|
||||
|
||||
;; Create a backtrace frames list with several frames.
|
||||
;; TODO load this from an el file in backtrace-resources/ so the tests
|
||||
;; can be byte-compiled.
|
||||
(defvar backtrace-tests--frames nil)
|
||||
|
||||
(defun backtrace-tests--func1 (arg1 arg2)
|
||||
(setq backtrace-tests--frames (backtrace-get-frames nil))
|
||||
(list arg1 arg2))
|
||||
|
||||
(defun backtrace-tests--func2 (arg)
|
||||
(list arg))
|
||||
|
||||
(defun backtrace-tests--func3 (arg)
|
||||
(let ((foo (list 'a arg 'b)))
|
||||
(list foo (backtrace-tests--func2 arg) (backtrace-tests--func1 arg 0))))
|
||||
|
||||
(defun backtrace-tests--create-backtrace-frames ()
|
||||
(backtrace-tests--func3 "string")
|
||||
;; Discard frames before this one.
|
||||
(let (this-index)
|
||||
(dotimes (index (length backtrace-tests--frames))
|
||||
(when (eq (backtrace-frame-fun (nth index backtrace-tests--frames))
|
||||
'backtrace-tests--create-backtrace-frames)
|
||||
(setq this-index index)))
|
||||
(setq backtrace-tests--frames (seq-subseq backtrace-tests--frames
|
||||
0 (1+ this-index)))))
|
||||
|
||||
(backtrace-tests--create-backtrace-frames)
|
||||
|
||||
;; TODO check that debugger-batch-max-lines still works
|
||||
|
||||
(defun backtrace-tests--insert-header ()
|
||||
(insert "Test header\n"))
|
||||
|
||||
(defmacro backtrace-tests--with-buffer (&rest body)
|
||||
`(with-temp-buffer
|
||||
(backtrace-mode)
|
||||
(setq backtrace-frames backtrace-tests--frames)
|
||||
(setq backtrace-insert-header-function #'backtrace-tests--insert-header)
|
||||
(backtrace-print)
|
||||
,@body))
|
||||
|
||||
;;; Tests
|
||||
(ert-deftest backtrace-tests--to-string ()
|
||||
(should (string= (backtrace-to-string backtrace-tests--frames)
|
||||
" backtrace-get-frames(nil)
|
||||
(setq backtrace-tests--frames (backtrace-get-frames nil))
|
||||
backtrace-tests--func1(\"string\" 0)
|
||||
(list foo (backtrace-tests--func2 arg) (backtrace-tests--func1 arg 0))
|
||||
(let ((foo (list 'a arg 'b))) (list foo (backtrace-tests--func2 arg) (backtrace-tests--func1 arg 0)))
|
||||
backtrace-tests--func3(\"string\")
|
||||
backtrace-tests--create-backtrace-frames()
|
||||
")))
|
||||
|
||||
(provide 'backtrace-tests)
|
||||
|
||||
;; These tests expect to see non-byte compiled stack frames.
|
||||
;; Local Variables:
|
||||
;; no-byte-compile: t
|
||||
;; End:
|
||||
|
||||
;;; backtrace-tests.el ends here
|
|
@ -376,7 +376,7 @@ This macro is used to test if macroexpansion in `should' works."
|
|||
(test (make-ert-test :body test-body))
|
||||
(result (ert-run-test test)))
|
||||
(should (ert-test-failed-p result))
|
||||
(should (eq (nth 1 (car (ert-test-failed-backtrace result)))
|
||||
(should (eq (backtrace-frame-fun (car (ert-test-failed-backtrace result)))
|
||||
'signal))))
|
||||
|
||||
(ert-deftest ert-test-messages ()
|
||||
|
|
Loading…
Add table
Reference in a new issue