├── pictures └── screencap.gif ├── .gitignore ├── README.org ├── test.txt └── erlstack-mode.el /pictures/screencap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k32/erlstack-mode/HEAD/pictures/screencap.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled 2 | *.elc 3 | 4 | # Packaging 5 | .cask 6 | 7 | # Backup files 8 | *~ 9 | 10 | # Undo-tree save-files 11 | *.~undo-tree 12 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Analyze Erlang stack traces 2 | 3 | [[https://melpa.org/#/erlstack-mode][file:https://melpa.org/packages/erlstack-mode-badge.svg]] 4 | 5 | [[file:pictures/screencap.gif]] 6 | 7 | Enable =erlstack-mode= globally to peek at the source code of 8 | functions appearing in Erlang stack traces: 9 | 10 | #+BEGIN_SRC elisp 11 | (require 'erlstack-mode) 12 | #+END_SRC 13 | 14 | Moving point to a stack trace will reveal code in question. This 15 | plugin works best with =projectile=, however it's not a hard 16 | requirement. 17 | 18 | * Key bindings 19 | 20 | The following key mappings are activated while point is on a stack trace: 21 | 22 | - =C-= Jump to the next stack frame 23 | - =C-= Jump to the previous stack frame 24 | - =C-= Open code for editing 25 | 26 | * Customizations 27 | 28 | The following variables can be customized: 29 | 30 | ** erlstack-file-search-hook 31 | 32 | A hook that is used to locate source code paths of Erlang modules 33 | 34 | ** erlstack-otp-src-path 35 | 36 | Path to the OTP source code. Customize this variable to locate OTP modules. 37 | 38 | ** erlstack-file-prefer-hook 39 | 40 | A hook that is called when =erlstack-file-search-hook= returns 41 | multiple paths for a module. It can be used to pick the preferred 42 | alternative 43 | -------------------------------------------------------------------------------- /test.txt: -------------------------------------------------------------------------------- 1 | Example stacktraces: 2 | 3 | [{shell,apply_fun,3,[{file,"shell.erl"},{line,907}]}, 4 | {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,681}]}, 5 | {erl_eval,try_clauses,8,[{file,"erl_eval.erl"},{line,911}]}, 6 | { shell , exprs , 7 , [{file,"shell.erl"},{line,686}]},{shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]}, 7 | {shell,eval_loop,3,[ {file,"shell.erl"}, {line,627}]}] 8 | 9 | =ERROR REPORT==== 21-Dec-2018::19:23:26.292922 === 10 | Error in process <0.88.0> with exit value: 11 | {1,[{shell,apply_fun,3,[{file,"shell.erl"},{line,907}]}]} 12 | 13 | 14 | 15 | [{file,"/store/Documents/Lee/src/lee_model.erl"}, 16 | {line,100}]}, 17 | {lee,get,3, 18 | [{file,"/store/Documents/Lee/src/lee.erl"},{line,176}]}, 19 | {lee,validate_value,4, 20 | [{file,"/store/Documents/Lee/src/lee.erl"},{line,253}]}, 21 | {lee,validate_moc_instances,4, 22 | [{file,"/store/Documents/Lee/src/lee.erl"},{line,243}]}, 23 | 24 | New logger style: 25 | ** exception error: no function clause matching 26 | xmerl_lib:expand_element(102,1, 27 | [{title,1},{article,1}], 28 | false) (xmerl_lib.erl, line 152) 29 | in function xmerl_lib:expand_content/4 (xmerl_lib.erl, line 225) 30 | in call from xmerl_lib:expand_element/4 (xmerl_lib.erl, line 188) 31 | in call from xmerl_lib:expand_content/4 (xmerl_lib.erl, line 225) 32 | in call from xmerl_lib:expand_element/4 (xmerl_lib.erl, line 188) 33 | in call from xmerl_lib:expand_content/4 (xmerl_lib.erl, line 225) 34 | in call from xmerl:export_simple1/3 (xmerl.erl, line 163) 35 | 36 | erlstack-mode.el ends here 37 | -------------------------------------------------------------------------------- /erlstack-mode.el: -------------------------------------------------------------------------------- 1 | ;;; erlstack-mode.el --- Minor mode for analysing Erlang stacktraces -*- lexical-binding: t; -*- 2 | 3 | ;; Author: k32 4 | ;; Keywords: tools, erlang 5 | ;; Version: 0.2.0 6 | ;; Homepage: https://github.com/k32/erlstack-mode 7 | ;; Package-Requires: ((emacs "25.1") (dash "2.12.0")) 8 | 9 | ;; This program is free software; you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; This program is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with this program. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; Enable `erlstack-mode' globally to peek at source code of functions 25 | ;; appearing in Erlang stack traces: 26 | ;; 27 | ;; (require 'erlstack-mode) 28 | ;; 29 | ;; Moving point to a stack trace will reveal code in question. This 30 | ;; plugin works best with projectile, however it’s not a hard 31 | ;; requirement. 32 | 33 | ;;; Code: 34 | 35 | (defgroup erlstack nil 36 | "Locate source code mentioned in `erlang' stacktraces." 37 | :group 'erlang 38 | :prefix "erlstack-") 39 | 40 | (require 'dash) 41 | 42 | ;;; Macros: 43 | 44 | (defvar erlstack--caches-global nil) 45 | 46 | (defvar erlstack--caches-local nil) 47 | 48 | (defmacro erlstack-define-cache (ctype name &rest args) 49 | "Create a new cache variable together with a getter function." 50 | (let ((cache-put-fun (pcase ctype 51 | ('local 'setq-local) 52 | ('global 'setq))) 53 | (getter-name (intern 54 | (concat "erlstack--cache-" 55 | (symbol-name name)))) 56 | (cache-name (intern 57 | (concat "erlstack--cache-" 58 | (symbol-name name) 59 | "-store"))) 60 | (caches-var (pcase ctype 61 | ('local 'erlstack--caches-local) 62 | ('global 'erlstack--caches-global)))) 63 | `(progn 64 | (,cache-put-fun ,cache-name ,(cons 'make-hash-table args)) 65 | (add-to-list (quote ,caches-var) (quote ,cache-name)) 66 | 67 | (defun ,getter-name (key fun) 68 | (let ((cached (gethash key ,cache-name nil))) 69 | (if cached 70 | cached 71 | (puthash key (eval fun) ,cache-name))))))) 72 | 73 | (defmacro erlstack-jump-frame (fun) 74 | "Helper macro searching for FUN." 75 | `(let* ((bound nil) ;;(,dir (point) erlstack-lookup-window)) 76 | (next-frame (save-excursion 77 | (erlstack-goto-stack-begin) 78 | ,fun))) 79 | (when next-frame 80 | (goto-char next-frame) 81 | (erlstack-goto-stack-begin)))) 82 | 83 | ;;; Variables: 84 | 85 | (defvar erlstack--overlay nil) 86 | 87 | (defvar erlstack--code-overlay nil) 88 | 89 | (defvar erlstack--code-window nil) 90 | 91 | (defvar erlstack--code-window-active nil) 92 | 93 | (defvar erlstack--code-buffer nil) 94 | 95 | (defvar-local erlstack--current-location nil) 96 | 97 | (defvar erlstack-frame-mode-map 98 | (make-sparse-keymap)) 99 | 100 | (define-key erlstack-frame-mode-map (kbd "C-") 'erlstack-visit-file) 101 | (define-key erlstack-frame-mode-map (kbd "C-") 'erlstack-up-frame) 102 | (define-key erlstack-frame-mode-map (kbd "C-") 'erlstack-down-frame) 103 | 104 | ;;; Regular expressions: 105 | 106 | (defun erlstack--whitespacify-concat (&rest re) 107 | "Intercalate strings with regexp RE matching whitespace." 108 | (--reduce (concat acc "[ \t\n]*" it) re)) 109 | 110 | (defvar erlstack--string-re 111 | "\"\\([^\"]*\\)\"") 112 | 113 | (defvar erlstack--file-re 114 | (erlstack--whitespacify-concat "{" "file" "," erlstack--string-re "}")) 115 | 116 | (defvar erlstack--line-re 117 | (erlstack--whitespacify-concat "{" "line" "," "\\([[:digit:]]+\\)" "}")) 118 | 119 | (defvar erlstack--position-re 120 | (erlstack--whitespacify-concat "\\[" erlstack--file-re "," erlstack--line-re "]")) 121 | 122 | (defvar erlstack--stack-frame-old-re 123 | (erlstack--whitespacify-concat erlstack--position-re "}")) 124 | (defvar erlstack--stack-frame-new-re 125 | (erlstack--whitespacify-concat "(\\(.+\\.erl\\)," "line" "\\([[:digit:]]+\\))")) 126 | (defvar erlstack--stack-frame-re 127 | (concat erlstack--stack-frame-old-re "\\|" erlstack--stack-frame-new-re)) 128 | 129 | (defvar erlstack--stack-end-new-re ")$") 130 | (defvar erlstack--stack-end-old-re "}]}") 131 | (defvar erlstack--stack-end-re 132 | (concat erlstack--stack-end-old-re 133 | "\\|" 134 | erlstack--stack-end-new-re)) 135 | 136 | ;;; Custom items: 137 | 138 | (defcustom erlstack-file-search-hook 139 | '(erlstack-locate-projectile 140 | erlstack-locate-otp 141 | erlstack-locate-abspath 142 | erlstack-locate-existing-buffer) 143 | "List of functions used to search the source code of the Erlang modules. 144 | Search runs these functions until one of them returns a result." 145 | :options '(erlstack-locate-abspath 146 | erlstack-locate-otp 147 | erlstack-locate-projectile 148 | erlstack-locate-existing-buffer) 149 | :group 'erlstack 150 | :type 'hook) 151 | 152 | (defcustom erlstack-file-prefer-hook 153 | '(erlstack-prefer-no-rebar-tmp) 154 | "A hook that is called when `erlstack-file-search-hook' returns 155 | multiple paths for a module. It can be used to pick the preferred 156 | alternative" 157 | :options '(erlstack-prefer-no-rebar-tmp 158 | erlstack-prefer-no-otp-dialyzer-files 159 | erlstack-prefer-library-modules) 160 | :group 'erlstack 161 | :type 'hook) 162 | 163 | (defcustom erlstack-lookup-window 300 164 | "Size of the lookup window." 165 | :group 'erlstack 166 | :type 'integer) 167 | 168 | (defcustom erlstack-otp-src-path "" 169 | "Path to the OTP source code." 170 | :group 'erlstack 171 | :type 'string) 172 | 173 | (defcustom erlstack-initial-delay 0.8 174 | "Overlay delay, in seconds" 175 | :group 'erlstack 176 | :type 'float) 177 | 178 | (defcustom erlstack-popup-window-alist 179 | '((display-buffer-reuse-window display-buffer-reuse-mode-window display-buffer-pop-up-window) 180 | ((mode . erlang) 181 | (inhibit-same-window . t))) 182 | "`display-buffer' alist used for the erlstack-mode preview window." 183 | :type 'sexp 184 | :group 'erlstack) 185 | 186 | ;;; Faces: 187 | 188 | (defface erlstack-active-frame 189 | '((((background light)) 190 | :background "orange" 191 | :foreground "darkred") 192 | (((background dark)) 193 | :background "orange" 194 | :foreground "red")) 195 | "Stack frame highlighting face") 196 | 197 | ;;; Internal functions: 198 | 199 | (defun erlstack--frame-found (begin end) 200 | "This fuction is called with arguments BEGIN END when point enters stack frame." 201 | (let ((query (match-string 1)) 202 | (line-number (string-to-number (match-string 2)))) 203 | ;; Hack: preserve initial state of the code window by restoring it 204 | (when (and erlstack--code-window-active (window-live-p erlstack--code-window)) 205 | (quit-restore-window erlstack--code-window)) 206 | (setq-local erlstack--current-location `(,query ,line-number)) 207 | (erlstack--try-show-file query line-number) 208 | (setq erlstack--overlay (make-overlay begin end)) 209 | (set-transient-map erlstack-frame-mode-map t) 210 | (overlay-put erlstack--overlay 'face 'erlstack-active-frame))) 211 | 212 | (defun erlstack--try-show-file (query line-number) 213 | "Search for the source code of module QUERY and navigate to LINE-NUMBER." 214 | (let* ((candidates 215 | (run-hook-with-args-until-success 'erlstack-file-search-hook query line-number)) 216 | (candidates- 217 | (--reduce-r-from (funcall it query line-number acc) 218 | candidates 219 | erlstack-file-prefer-hook)) 220 | (filename 221 | (car (if candidates- 222 | candidates- 223 | candidates)))) 224 | (if filename 225 | (progn 226 | (erlstack--code-popup filename line-number)) 227 | (erlstack--frame-lost)))) 228 | 229 | (defun erlstack--code-popup (filename line-number) 230 | "Open a pop-up window with the code of FILENAME at LINE-NUMBER." 231 | (setq erlstack--code-buffer (find-file-noselect filename t)) 232 | (with-current-buffer erlstack--code-buffer 233 | (with-no-warnings 234 | (goto-line line-number)) 235 | (setq erlstack--code-buffer-posn (point)) 236 | (setq erlstack--code-overlay (make-overlay 237 | (line-beginning-position) 238 | (line-end-position))) 239 | (overlay-put erlstack--code-overlay 'face 'erlstack-active-frame) 240 | (setq erlstack--code-window (display-buffer erlstack--code-buffer erlstack-popup-window-alist)) 241 | (setq erlstack--code-window-active t) 242 | (set-window-point erlstack--code-window erlstack--code-buffer-posn))) 243 | 244 | (defun erlstack-visit-file () 245 | "Open file related to the currently selected stack frame for editing." 246 | (interactive) 247 | (when erlstack--code-window-active 248 | (setq erlstack--code-window-active nil) 249 | (pcase erlstack--current-location 250 | (`(,_filename ,line-number) 251 | (select-window erlstack--code-window) 252 | (with-no-warnings 253 | (goto-line line-number)))))) 254 | 255 | (defun erlstack--frame-lost () 256 | "This fuction is called when point leaves stack frame." 257 | (when erlstack--code-window-active 258 | (unless (eq erlstack--code-window (selected-window)) 259 | (quit-restore-window erlstack--code-window)) 260 | (setq erlstack--code-window-active nil))) 261 | 262 | (defun erlstack-run-at-point () 263 | "Attempt to analyse stack frame at the point." 264 | (interactive) 265 | (run-with-idle-timer 266 | (if erlstack--code-window-active 267 | 0.1 268 | erlstack-initial-delay) nil 269 | (lambda () 270 | (when erlstack--overlay 271 | (delete-overlay erlstack--overlay)) 272 | (when erlstack--code-overlay 273 | (delete-overlay erlstack--code-overlay)) 274 | (pcase (erlstack--parse-at-point) 275 | (`(,begin ,end) (erlstack--frame-found begin end)) 276 | (_ (erlstack--frame-lost)))))) 277 | 278 | (defun erlstack--re-search-backward () 279 | (let ((bound (save-excursion (forward-line -2) 280 | (line-beginning-position)))) 281 | (or 282 | (re-search-backward erlstack--stack-frame-old-re bound t) 283 | (re-search-backward erlstack--stack-frame-new-re bound t)))) 284 | 285 | (defun erlstack--parse-at-point () 286 | "Attempt to find stacktrace at point." 287 | (save-excursion 288 | (let ((point (point)) 289 | (end (re-search-forward erlstack--stack-end-re 290 | (save-excursion (forward-line 2) 291 | (line-end-position)) 292 | t)) 293 | (begin (erlstack--re-search-backward))) 294 | (when (and begin end (>= point begin)) 295 | `(,begin ,end))))) 296 | 297 | ;;; Stack navigation: 298 | 299 | (defun erlstack-goto-stack-begin () 300 | "Jump to the beginning of stack frame." 301 | (goto-char (nth 0 (erlstack--parse-at-point)))) 302 | 303 | (defun erlstack-goto-stack-end () 304 | "Jump to the end of stack frame." 305 | (goto-char (nth 1 (erlstack--parse-at-point)))) 306 | 307 | (defun erlstack-rebar-tmp-dirp (path) 308 | "Pretty dumb check that `path' is a temporary file created by 309 | rebar3. This function returns parent directory of rebar's temp 310 | drectory or `nil' otherwise." 311 | (let ((up (directory-file-name (file-name-directory path))) 312 | (dir (file-name-nondirectory path))) 313 | (if (string= up path) 314 | nil 315 | (if (string= dir "_build") 316 | up 317 | (erlstack-rebar-tmp-dirp up))))) 318 | 319 | (defun erlstack-prefer-no-rebar-tmp (_query _line-number candidates) 320 | "Remove rebar3 temporary files when the originals are found in the list." 321 | (interactive) 322 | ;; TODO check that the removed files actually match with the ones 323 | ;; that left? 324 | (if (cdr candidates) 325 | (--filter (not (erlstack-rebar-tmp-dirp it)) candidates) 326 | candidates)) 327 | 328 | (defun erlstack-prefer-library-modules (_query _line-number candidates) 329 | "Prefer OTP library modules over mocks" 330 | (pcase (-separate 'erlstack--is-library-module candidates) 331 | (`(,a ,b) 332 | (append a b)))) 333 | 334 | (defun erlstack--is-library-module (file) 335 | (string= "src" (file-name-nondirectory 336 | (directory-file-name (file-name-directory file))))) 337 | 338 | (defun erlstack-prefer-no-otp-dialyzer-files (_query _line-number candidates) 339 | "Filter out dialyzer test modules from the list of OTP files." 340 | (--filter (not (string-match "test/.*SUITE_data/" it)) candidates)) 341 | 342 | (defun erlstack-up-frame () 343 | "Move one stack frame up." 344 | (interactive) 345 | (erlstack-jump-frame 346 | (re-search-backward erlstack--stack-frame-re bound t))) 347 | 348 | (defun erlstack-down-frame () 349 | "Move one stack frame down." 350 | (interactive) 351 | (erlstack-jump-frame 352 | (progn 353 | (re-search-forward erlstack--stack-frame-re bound t 2) 354 | (re-search-backward erlstack--stack-frame-re bound t)))) ;; TODO: refactor this hack \^//// 355 | 356 | ;;; User commands: 357 | 358 | (defun erlstack-drop-caches () 359 | "Drop all `erlstack' caches." 360 | (interactive) 361 | (dolist (it erlstack--caches-global) 362 | (clrhash (eval it)) 363 | (message "erlstack-mode: Cleared %s" it)) 364 | (dolist (buff (buffer-list)) 365 | (dolist (var erlstack--caches-local) 366 | (with-current-buffer buff 367 | (when (boundp var) 368 | (clrhash (eval var)) 369 | (message "erlstack-mode: Cleared %s in %s" var buff)))))) 370 | 371 | ;;; Locate source hooks: 372 | 373 | (defun erlstack-locate-abspath (query _line) 374 | "Try locating module QUERY by absolute path." 375 | (when (file-exists-p query) 376 | (list query))) 377 | 378 | (erlstack-define-cache global otp-files 379 | :test 'equal) 380 | 381 | (defun erlstack-locate-otp (query _line) 382 | "Try searching for module QUERY in the OTP sources." 383 | (let ((query- (file-name-nondirectory query))) 384 | (when (and (string> erlstack-otp-src-path "") 385 | (file-directory-p erlstack-otp-src-path)) 386 | (erlstack--cache-otp-files 387 | query- 388 | `(directory-files-recursively erlstack-otp-src-path 389 | ,(concat "^" query- "$")))))) 390 | 391 | (erlstack-define-cache global projectile 392 | :test 'equal) 393 | 394 | (defun erlstack-locate-projectile (query _line) 395 | "Try searching for module QUERY in the current `projectile' root." 396 | (when (fboundp 'projectile-project-root) 397 | (let ((dir (projectile-project-root)) 398 | (query- (file-name-nondirectory query))) 399 | (when dir 400 | (erlstack--cache-projectile 401 | (list dir query) 402 | `(directory-files-recursively 403 | ,dir 404 | ,(concat "^" query- "$"))))))) 405 | 406 | (defun erlstack-locate-existing-buffer (query _line) 407 | "Try matching existing buffers with QUERY." 408 | (let ((query- (file-name-nondirectory query))) 409 | (--filter 410 | (string= query- (file-name-nondirectory it)) 411 | (--filter it (--map (buffer-file-name it) (buffer-list)))))) 412 | 413 | (define-minor-mode erlstack-mode 414 | "Parse Erlang stacktrace at the point and quickly navigate to 415 | the line of the code" 416 | :keymap nil 417 | :group 'erlstack 418 | :lighter " \u2622" 419 | :global t 420 | :require 'erlstack-mode 421 | :group 'erlstack 422 | (if erlstack-mode 423 | (add-hook 'post-command-hook #'erlstack-run-at-point) 424 | (remove-hook 'post-command-hook #'erlstack-run-at-point))) 425 | 426 | (provide 'erlstack-mode) 427 | 428 | ;;; erlstack-mode.el ends here 429 | --------------------------------------------------------------------------------