|
| 1 | +;;; cider-stacktrace.el --- Stacktrace navigator -*- lexical-binding: t -*- |
| 2 | + |
| 3 | +;; Copyright © 2014 Jeff Valk |
| 4 | + |
| 5 | +;; Author: Jeff Valk <[email protected]> |
| 6 | + |
| 7 | +;; This program is free software: you can redistribute it and/or modify |
| 8 | +;; it under the terms of the GNU General Public License as published by |
| 9 | +;; the Free Software Foundation, either version 3 of the License, or |
| 10 | +;; (at your option) any later version. |
| 11 | + |
| 12 | +;; This program is distributed in the hope that it will be useful, |
| 13 | +;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | +;; GNU General Public License for more details. |
| 16 | + |
| 17 | +;; You should have received a copy of the GNU General Public License |
| 18 | +;; along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 19 | + |
| 20 | +;; This file is not part of GNU Emacs. |
| 21 | + |
| 22 | +;;; Commentary: |
| 23 | + |
| 24 | +;; Stacktrace filtering and stack frame source navigation |
| 25 | + |
| 26 | +;;; Code: |
| 27 | + |
| 28 | +(require 'button) |
| 29 | + |
| 30 | + |
| 31 | +;; Variables |
| 32 | + |
| 33 | +(defgroup cider-stacktrace nil |
| 34 | + "Stacktrace filtering and navigation." |
| 35 | + :prefix "cider-stacktrace-" |
| 36 | + :group 'cider) |
| 37 | + |
| 38 | +(defcustom cider-stacktrace-default-filters '(tooling dup) |
| 39 | + "Frame types to omit from initial stacktrace display." |
| 40 | + :type 'list |
| 41 | + :group 'cider-stacktrace) |
| 42 | + |
| 43 | + |
| 44 | +;; Faces |
| 45 | + |
| 46 | +(defface cider-stacktrace-error-class-face |
| 47 | + '((t (:inherit font-lock-warning-face))) |
| 48 | + "Face for exception class names" |
| 49 | + :group 'cider-stacktrace) |
| 50 | + |
| 51 | +(defface cider-stacktrace-filter-shown-face |
| 52 | + '((t (:inherit button :underline t))) |
| 53 | + "Face for filter buttons representing frames currently visible" |
| 54 | + :group 'cider-stacktrace) |
| 55 | + |
| 56 | +(defface cider-stacktrace-filter-hidden-face |
| 57 | + '((t (:inherit button :underline nil))) |
| 58 | + "Face for filter buttons representing frames currently filtered out" |
| 59 | + :group 'cider-stacktrace) |
| 60 | + |
| 61 | +(defface cider-stacktrace-face |
| 62 | + '((t (:inherit default))) |
| 63 | + "Face for stack frame text" |
| 64 | + :group 'cider-stacktrace) |
| 65 | + |
| 66 | +(defface cider-stacktrace-ns-face |
| 67 | + '((t (:inherit font-lock-comment-face))) |
| 68 | + "Face for stack frame namespace name" |
| 69 | + :group 'cider-stacktrace) |
| 70 | + |
| 71 | +(defface cider-stacktrace-fn-face |
| 72 | + '((t (:inherit cider-stacktrace-face :weight bold))) |
| 73 | + "Face for stack frame function name" |
| 74 | + :group 'cider-stacktrace) |
| 75 | + |
| 76 | + |
| 77 | +;; Mode & key bindings |
| 78 | + |
| 79 | +(defvar cider-stacktrace-mode-map |
| 80 | + (let ((map (make-sparse-keymap))) |
| 81 | + (define-key map "j" 'cider-stacktrace-toggle-java) |
| 82 | + (define-key map "c" 'cider-stacktrace-toggle-clj) |
| 83 | + (define-key map "r" 'cider-stacktrace-toggle-repl) |
| 84 | + (define-key map "t" 'cider-stacktrace-toggle-tooling) |
| 85 | + (define-key map "d" 'cider-stacktrace-toggle-duplicates) |
| 86 | + (define-key map "a" 'cider-stacktrace-show-all) |
| 87 | + map)) |
| 88 | + |
| 89 | +(define-minor-mode cider-stacktrace-mode |
| 90 | + "CIDER Stacktrace Buffer Mode." |
| 91 | + nil |
| 92 | + (" CIDER Stacktrace") |
| 93 | + cider-stacktrace-mode-map |
| 94 | + (setq buffer-read-only t) |
| 95 | + (setq-local truncate-lines t) |
| 96 | + (setq-local cider-stacktrace-hidden-frame-count 0) |
| 97 | + (setq-local cider-stacktrace-filters cider-stacktrace-default-filters)) |
| 98 | + |
| 99 | + |
| 100 | +;; Stacktrace filtering |
| 101 | + |
| 102 | +(defun cider-stacktrace-indicate-filters (filters) |
| 103 | + "Update enabled state of filter buttons. Find buttons with a 'filter property; |
| 104 | +if filter is a member of FILTERS, or if filter is nil ('show all') and the |
| 105 | +argument list is non-nil, fontify the button as disabled. Upon finding text with |
| 106 | +a 'hidden-count property, stop searching and update the hidden count text." |
| 107 | + (with-current-buffer (get-buffer cider-error-buffer) |
| 108 | + (save-excursion |
| 109 | + (goto-char (point-min)) |
| 110 | + (let ((inhibit-read-only t) |
| 111 | + (get-face (lambda (hide) |
| 112 | + (if hide |
| 113 | + 'cider-stacktrace-filter-hidden-face |
| 114 | + 'cider-stacktrace-filter-shown-face)))) |
| 115 | + ;; Toggle buttons |
| 116 | + (while (not (or (get-text-property (point) 'hidden-count) (eobp))) |
| 117 | + (let ((button (button-at (point)))) |
| 118 | + (when button |
| 119 | + (let* ((filter (button-get button 'filter)) |
| 120 | + (face (funcall get-face (if filter |
| 121 | + (member filter filters) |
| 122 | + filters)))) |
| 123 | + (button-put button 'face face))) |
| 124 | + (goto-char (or (next-property-change (point)) |
| 125 | + (point-max))))) |
| 126 | + ;; Update hidden count |
| 127 | + (when (and (get-text-property (point) 'hidden-count) |
| 128 | + (re-search-forward "[0-9]+" (line-end-position) t)) |
| 129 | + (replace-match |
| 130 | + (number-to-string cider-stacktrace-hidden-frame-count))))))) |
| 131 | + |
| 132 | +(defun cider-stacktrace-apply-filters (filters) |
| 133 | + "Set visibility on stack frames using FILTERS. |
| 134 | +Update `cider-stacktrace-hidden-frame-count' and indicate filters applied." |
| 135 | + (with-current-buffer (get-buffer cider-error-buffer) |
| 136 | + (save-excursion |
| 137 | + (goto-char (point-min)) |
| 138 | + (let ((inhibit-read-only t) |
| 139 | + (hidden 0)) |
| 140 | + (while (not (eobp)) |
| 141 | + (let* ((flags (get-text-property (point) 'flags)) |
| 142 | + (hide (if (intersection filters flags) t nil))) |
| 143 | + (when hide (setq hidden (+ 1 hidden))) |
| 144 | + (put-text-property (point) (line-beginning-position 2) 'invisible hide)) |
| 145 | + (forward-line 1)) |
| 146 | + (setq cider-stacktrace-hidden-frame-count hidden))) |
| 147 | + (cider-stacktrace-indicate-filters filters))) |
| 148 | + |
| 149 | + |
| 150 | +;; Interactive functions |
| 151 | + |
| 152 | +(defun cider-stacktrace-show-all () |
| 153 | + "Reset `cider-stacktrace-filters', and apply filters." |
| 154 | + (interactive) |
| 155 | + (cider-stacktrace-apply-filters |
| 156 | + (setq cider-stacktrace-filters nil))) |
| 157 | + |
| 158 | +(defun cider-stacktrace-toggle (flag) |
| 159 | + "Update `cider-stacktrace-filters' to add or remove FLAG, and apply filters." |
| 160 | + (cider-stacktrace-apply-filters |
| 161 | + (setq cider-stacktrace-filters |
| 162 | + (if (memq flag cider-stacktrace-filters) |
| 163 | + (remq flag cider-stacktrace-filters) |
| 164 | + (cons flag cider-stacktrace-filters))))) |
| 165 | + |
| 166 | +(defun cider-stacktrace-toggle-java () |
| 167 | + "Toggle display of Java stack frames." |
| 168 | + (interactive) |
| 169 | + (cider-stacktrace-toggle 'java)) |
| 170 | + |
| 171 | +(defun cider-stacktrace-toggle-clj () |
| 172 | + "Toggle display of Clojure stack frames." |
| 173 | + (interactive) |
| 174 | + (cider-stacktrace-toggle 'clj)) |
| 175 | + |
| 176 | +(defun cider-stacktrace-toggle-repl () |
| 177 | + "Toggle display of REPL stack frames." |
| 178 | + (interactive) |
| 179 | + (cider-stacktrace-toggle 'repl)) |
| 180 | + |
| 181 | +(defun cider-stacktrace-toggle-tooling () |
| 182 | + "Toggle display of Tooling stack frames (compiler, nREPL middleware, etc)." |
| 183 | + (interactive) |
| 184 | + (cider-stacktrace-toggle 'tooling)) |
| 185 | + |
| 186 | +(defun cider-stacktrace-toggle-duplicates () |
| 187 | + "Toggle display of stack frames that are duplicates of their descendents." |
| 188 | + (interactive) |
| 189 | + (cider-stacktrace-toggle 'dup)) |
| 190 | + |
| 191 | + |
| 192 | +;; Text button functions |
| 193 | + |
| 194 | +(defun cider-stacktrace-filter (button) |
| 195 | + "Apply filter(s) indicated by the BUTTON." |
| 196 | + (with-temp-message "Filters may also be toggled with the keyboard." |
| 197 | + (let ((flag (button-get button 'filter))) |
| 198 | + (if flag |
| 199 | + (cider-stacktrace-toggle flag) |
| 200 | + (cider-stacktrace-show-all))) |
| 201 | + (sit-for 5))) |
| 202 | + |
| 203 | +(defun cider-stacktrace-navigate (button) |
| 204 | + "Navigate to the stack frame represented by the BUTTON." |
| 205 | + (let ((var (button-get button 'var)) |
| 206 | + (line (button-get button 'line))) |
| 207 | + (condition-case nil |
| 208 | + (let* ((info (cider-var-info var)) |
| 209 | + (file (cadr (assoc "file" info)))) |
| 210 | + (cider-jump-to-def-for (vector file file line))) |
| 211 | + (error "No source info")))) |
| 212 | + |
| 213 | + |
| 214 | +;; Rendering |
| 215 | + |
| 216 | +(defun cider-stacktrace-render-cause (buffer cause note) |
| 217 | + "Emit into BUFFER the CAUSE exception class, message, and data, and NOTE." |
| 218 | + (with-current-buffer buffer |
| 219 | + (nrepl-dbind-response cause (class message data) |
| 220 | + (put-text-property 0 (length class) |
| 221 | + 'font-lock-face |
| 222 | + 'cider-stacktrace-error-class-face |
| 223 | + class) |
| 224 | + (insert note " " class " " message) |
| 225 | + (newline) |
| 226 | + (when data |
| 227 | + (insert (cider-font-lock-as-clojure data)) |
| 228 | + (newline))))) |
| 229 | + |
| 230 | +(defun cider-stacktrace-render-filters (buffer filters) |
| 231 | + "Emit into BUFFER toggle buttons for each of the FILTERS." |
| 232 | + (with-current-buffer buffer |
| 233 | + (insert " Show: ") |
| 234 | + (dolist (filter filters) |
| 235 | + (insert-text-button (first filter) |
| 236 | + 'filter (second filter) |
| 237 | + 'follow-link t |
| 238 | + 'action 'cider-stacktrace-filter |
| 239 | + 'help-echo (format "Toggle %s stack frames" |
| 240 | + (first filter))) |
| 241 | + (insert " ")) |
| 242 | + (let ((hidden "(0 frames hidden)")) |
| 243 | + (put-text-property 0 (length hidden) 'hidden-count t hidden) |
| 244 | + (insert " " hidden)) |
| 245 | + (newline))) |
| 246 | + |
| 247 | +(defun cider-stacktrace-render-frame (buffer frame) |
| 248 | + "Emit into BUFFER function call site info for the stack FRAME. |
| 249 | +This associates text properties to enable filtering and source navigation." |
| 250 | + (with-current-buffer buffer |
| 251 | + (nrepl-dbind-response frame (file line flags class method name var ns fn) |
| 252 | + (let ((flags (mapcar 'intern flags))) ; strings -> symbols |
| 253 | + (insert-text-button (format "%30s:%5d %s/%s" |
| 254 | + (if (member 'repl flags) "REPL" file) line |
| 255 | + (if (member 'clj flags) ns class) |
| 256 | + (if (member 'clj flags) fn method)) |
| 257 | + 'name name 'var var 'line line 'flags flags |
| 258 | + 'follow-link t |
| 259 | + 'action 'cider-stacktrace-navigate |
| 260 | + 'help-echo "View source at this location" |
| 261 | + 'face 'cider-stacktrace-face) |
| 262 | + (save-excursion |
| 263 | + (let ((p3 (point)) |
| 264 | + (p1 (search-backward " ")) |
| 265 | + (p2 (search-forward "/"))) |
| 266 | + (put-text-property p1 p2 'face 'cider-stacktrace-ns-face) |
| 267 | + (put-text-property p2 p3 'face 'cider-stacktrace-fn-face))) |
| 268 | + (newline))))) |
| 269 | + |
| 270 | +(defun cider-stacktrace-render (buffer causes frames) |
| 271 | + "Emit into BUFFER useful stacktrace information for the CAUSES and FRAMES." |
| 272 | + (with-current-buffer buffer |
| 273 | + (let ((inhibit-read-only t)) |
| 274 | + ;; Exceptions |
| 275 | + (cider-stacktrace-render-cause buffer (first causes) "Unhandled") |
| 276 | + (dolist (cause (rest causes)) |
| 277 | + (cider-stacktrace-render-cause buffer cause "Caused by")) |
| 278 | + (newline) |
| 279 | + ;; Stacktrace filters |
| 280 | + (cider-stacktrace-render-filters |
| 281 | + buffer |
| 282 | + `(("Clojure" clj) ("Java" java) ("REPL" repl) |
| 283 | + ("Tooling" tooling) ("Duplicates" dup) ("All" ,nil))) |
| 284 | + (newline) |
| 285 | + ;; Stacktrace frames |
| 286 | + (dolist (frame frames) |
| 287 | + (cider-stacktrace-render-frame buffer frame))) |
| 288 | + ;; Set mode, apply filters, move point to first stacktrace frame. |
| 289 | + (cider-stacktrace-mode 1) |
| 290 | + (cider-stacktrace-apply-filters cider-stacktrace-filters) |
| 291 | + (goto-char (next-single-property-change (point-min) 'flags)) |
| 292 | + (font-lock-refresh-defaults))) |
| 293 | + |
| 294 | +(provide 'cider-stacktrace) |
| 295 | + |
| 296 | +;;; cider-stacktrace.el ends here |
0 commit comments