Add new command to expand all "..."s in a backtrace frame

* doc/lispref/debugging.texi (Backtraces): Document new keybinding.
* lisp/emacs-lisp/backtrace.el (backtrace-line-length): Add the
option of unlimited line length.
(backtrace--match-ellipsis-in-string): Add a comment to explain
why this function is necessary.
(backtrace-mode-map): Add keybinding for 'backtrace-expand-ellipses'.
(backtrace-expand-ellipsis): Use 'cl-print-to-string-with-limit'.
(backtrace-expand-ellipses): New command.
(backtrace-print-to-string): Use 'cl-print-to-string-with-limit'.
Tag the printed forms with a gensym instead of the values of
print-length and print-level.
(backtrace--print): Add 'stream' argument.
* test/lisp/emacs-lisp/backtrace-tests.el
(backtrace-tests--expand-ellipsis): Make the test less dependent
on the implementation.
(backtrace-tests--expand-ellipses): New test.

Move the fitting of a printed representation into a limited number of
characters using appropriate values of print-level and print-length
from 'backtrace-print-to-string' to cl-print.el for future use by
other parts of Emacs.
* lisp/emacs-lisp/cl-print.el (cl-print-to-string-with-limit): New
function.
* test/lisp/emacs-lisp/cl-print-tests.el
(cl-print-tests-print-to-string-with-limit): New test.
This commit is contained in:
Gemini Lasswell 2018-07-14 08:05:51 -07:00
parent 2ede75c49b
commit a3ba34aeac
5 changed files with 192 additions and 71 deletions

View file

@ -457,6 +457,9 @@ Collapse the top-level Lisp form at point back to a single line.
@item #
Toggle @code{print-circle} for the frame at point.
@item .
Expand all the forms abbreviated with ``...'' in the frame at point.
@end table
@node Debugger Commands

View file

@ -55,7 +55,8 @@ order to debug the code that does fontification."
"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."
guaranteed. If set to nil or zero, Backtrace mode will not
abbreviate the forms it prints."
:type 'integer
:group 'backtrace
:version "27.1")
@ -146,6 +147,9 @@ fontifies.")
(defun backtrace--match-ellipsis-in-string (bound)
;; Fontify ellipses within strings as buttons.
;; This is necessary because ellipses are text property buttons
;; instead of overlay buttons, which is done because there could
;; be a large number of them.
(when (re-search-forward "\\(\\.\\.\\.\\)\"" bound t)
(and (get-text-property (- (point) 2) 'cl-print-ellipsis)
(get-text-property (- (point) 3) 'cl-print-ellipsis)
@ -187,6 +191,7 @@ This is commonly used to recompute `backtrace-frames'.")
(define-key map "\C-m" 'backtrace-help-follow-symbol)
(define-key map "+" 'backtrace-pretty-print)
(define-key map "-" 'backtrace-collapse)
(define-key map "." 'backtrace-expand-ellipses)
(define-key map [follow-link] 'mouse-face)
(define-key map [mouse-2] 'mouse-select-window)
map)
@ -207,9 +212,7 @@ This is commonly used to recompute `backtrace-frames'.")
;; 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.
;; found for pretty-printing.
(defsubst backtrace-get-index (&optional pos)
"Return the index of the backtrace frame at POS.
@ -423,9 +426,6 @@ Reprint the frame with the new view plist."
(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)
@ -437,25 +437,44 @@ Reprint the frame with the new view plist."
(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)))))
(delete-region begin end)
(insert (cl-print-to-string-with-limit #'cl-print-expand-ellipsis value
backtrace-line-length))
(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-expand-ellipses (&optional no-limit)
"Expand display of all \"...\"s in the backtrace frame at point.
\\<backtrace-mode-map>
Each ellipsis will be limited to `backtrace-line-length'
characters in its expansion. With optional prefix argument
NO-LIMIT, do not limit the number of characters. Note that with
or without the argument, using this command can result in very
long lines and very poor display performance. If this happens
and is a problem, use `\\[revert-buffer]' to return to the
initial state of the Backtrace buffer."
(interactive "P")
(save-excursion
(let ((start (backtrace-get-frame-start))
(end (backtrace-get-frame-end))
(backtrace-line-length (unless no-limit backtrace-line-length)))
(goto-char end)
(while (> (point) start)
(let ((next (previous-single-property-change (point) 'cl-print-ellipsis
nil start)))
(when (get-text-property (point) 'cl-print-ellipsis)
(push-button (point)))
(goto-char next))))))
(defun backtrace-pretty-print ()
"Pretty-print the top level s-expression at point."
@ -605,8 +624,7 @@ line and recenter window line accordingly."
"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'."
`print-length.' LIMIT defaults to `backtrace-line-length'."
(backtrace--with-output-variables backtrace-view
(backtrace--print-to-string obj limit)))
@ -614,36 +632,20 @@ charcters with appropriate settings of `print-level' and
;; 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))))
(with-temp-buffer
(insert (cl-print-to-string-with-limit #'backtrace--print sexp limit))
;; Add a unique backtrace-form property.
(put-text-property (point-min) (point) 'backtrace-form (gensym))
;; Make buttons from all the "..."s. Since there might be many of
;; them, use text property buttons.
(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.
@ -727,14 +729,14 @@ Print them only if :show-locals is non-nil in the VIEW plist."
(insert "\n")))
(put-text-property beg (point) 'backtrace-section 'locals))))
(defun backtrace--print (obj)
"Attempt to print OBJ using `backtrace-print-function'.
(defun backtrace--print (obj &optional stream)
"Attempt to print OBJ to STREAM using `backtrace-print-function'.
Fall back to `prin1' if there is an error."
(condition-case err
(funcall backtrace-print-function obj)
(funcall backtrace-print-function obj stream)
(error
(message "Error in backtrace printer: %S" err)
(prin1 obj))))
(prin1 obj stream))))
(defun backtrace-update-flags ()
"Update the display of the flags in the backtrace frame at point."
@ -805,8 +807,6 @@ followed by `backtrace-print-frame', once for each stack frame."
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)

View file

@ -524,5 +524,45 @@ node `(elisp)Output Variables'."
(cl-prin1 object (current-buffer))
(buffer-string)))
;;;###autoload
(defun cl-print-to-string-with-limit (print-function value limit)
"Return a string containing a printed representation of VALUE.
Attempt to get the length of the returned string under LIMIT
characters with appropriate settings of `print-level' and
`print-length.' Use PRINT-FUNCTION to print, which should take
the arguments VALUE and STREAM and which should respect
`print-length' and `print-level'. LIMIT may be nil or zero in
which case PRINT-FUNCTION will be called with `print-level' and
`print-length' bound to nil.
Use this function with `cl-prin1' to print an object,
abbreviating it with ellipses to fit within a size limit. Use
this function with `cl-prin1-expand-ellipsis' to expand an
ellipsis, abbreviating the expansion to stay within a size
limit."
(setq limit (and (natnump limit)
(not (zerop limit))
limit))
;; Since this is used by the debugger when stack space may be
;; limited, if you increase print-level here, add more depth in
;; call_debugger (bug#31919).
(let* ((print-length (when limit (min limit 50)))
(print-level (when limit (min 8 (truncate (log limit)))))
(delta (when limit
(max 1 (truncate (/ print-length print-level))))))
(with-temp-buffer
(catch 'done
(while t
(erase-buffer)
(funcall print-function value (current-buffer))
;; Stop when either print-level is too low or the value is
;; successfully printed in the space allowed.
(when (or (not limit)
(< (- (point-max) (point-min)) limit)
(= print-level 2))
(throw 'done (buffer-string)))
(cl-decf print-level)
(cl-decf print-length delta))))))
(provide 'cl-print)
;;; cl-print.el ends here

View file

@ -349,32 +349,74 @@ digit and replace with #[0-9]."
(buffer-string)))
(ert-deftest backtrace-tests--expand-ellipsis ()
"Backtrace buffers ellipsify large forms and can expand the ellipses."
"Backtrace buffers ellipsify large forms as buttons which expand the ellipses."
;; make a backtrace with an ellipsis
;; expand the ellipsis
(ert-with-test-buffer (:name "variables")
(let* ((print-level nil)
(print-length nil)
(arg (let ((long (make-list 100 'a))
(deep '(0 (1 (2 (3 (4 (5 (6 (7 (8 (9))))))))))))
(setf (nth 1 long) deep)
long))
(backtrace-line-length 300)
(arg (make-list 40 (make-string 10 ?a)))
(results (backtrace-tests--result arg)))
(backtrace-tests--make-backtrace arg)
(backtrace-print)
;; There should be two ellipses. Find and expand them.
;; There should be an ellipsis. Find and expand it.
(goto-char (point-min))
(search-forward "...")
(backward-char)
(push-button)
(search-forward "...")
(backward-char)
(push-button)
(should (string= (backtrace-tests--get-substring (point-min) (point-max))
results)))))
(ert-deftest backtrace-tests--expand-ellipses ()
"Backtrace buffers ellipsify large forms and can expand the ellipses."
(ert-with-test-buffer (:name "variables")
(let* ((print-level nil)
(print-length nil)
(backtrace-line-length 300)
(arg (let ((outer (make-list 40 (make-string 10 ?a)))
(nested (make-list 40 (make-string 10 ?b))))
(setf (nth 39 nested) (make-list 40 (make-string 10 ?c)))
(setf (nth 39 outer) nested)
outer))
(results (backtrace-tests--result-with-locals arg)))
;; Make a backtrace with local variables visible.
(backtrace-tests--make-backtrace arg)
(backtrace-print)
(backtrace-toggle-locals '(4))
;; There should be two ellipses.
(goto-char (point-min))
(should (search-forward "..."))
(should (search-forward "..."))
(should-error (search-forward "..."))
;; Expanding the last frame without argument should expand both
;; ellipses, but the expansions will contain one ellipsis each.
(let ((buffer-len (- (point-max) (point-min))))
(goto-char (point-max))
(backtrace-backward-frame)
(backtrace-expand-ellipses)
(should (> (- (point-max) (point-min)) buffer-len))
(goto-char (point-min))
(should (search-forward "..."))
(should (search-forward "..."))
(should-error (search-forward "...")))
;; Expanding with argument should remove all ellipses.
(goto-char (point-max))
(backtrace-backward-frame)
(backtrace-expand-ellipses '(4))
(goto-char (point-min))
(should-error (search-forward "..."))
(should (string= (backtrace-tests--get-substring (point-min) (point-max))
results)))))
(ert-deftest backtrace-tests--to-string ()
"Backtraces can be produced as strings."
(let ((frames (ert-with-test-buffer (:name nil)

View file

@ -233,5 +233,41 @@
(let ((print-circle t))
(should (equal "(0 . #1=(0 . #1#))" (cl-prin1-to-string x))))))
(ert-deftest cl-print-tests-print-to-string-with-limit ()
(let* ((thing10 (make-list 10 'a))
(thing100 (make-list 100 'a))
(thing10x10 (make-list 10 thing10))
(nested-thing (let ((val 'a))
(dotimes (_i 20)
(setq val (list val)))
val))
;; Make a consistent environment for this test.
(print-circle nil)
(print-level nil)
(print-length nil))
;; Print something that fits in the space given.
(should (string= (cl-prin1-to-string thing10)
(cl-print-to-string-with-limit #'cl-prin1 thing10 100)))
;; Print something which needs to be abbreviated and which can be.
(should (< (length (cl-print-to-string-with-limit #'cl-prin1 thing100 100))
100
(length (cl-prin1-to-string thing100))))
;; Print something resistant to easy abbreviation.
(should (string= (cl-prin1-to-string thing10x10)
(cl-print-to-string-with-limit #'cl-prin1 thing10x10 100)))
;; Print something which should be abbreviated even if the limit is large.
(should (< (length (cl-print-to-string-with-limit #'cl-prin1 nested-thing 1000))
(length (cl-prin1-to-string nested-thing))))
;; Print with no limits.
(dolist (thing (list thing10 thing100 thing10x10 nested-thing))
(let ((rep (cl-prin1-to-string thing)))
(should (string= rep (cl-print-to-string-with-limit #'cl-prin1 thing 0)))
(should (string= rep (cl-print-to-string-with-limit #'cl-prin1 thing nil)))))))
;;; cl-print-tests.el ends here.