cider-inspector.el (22551B)
1 ;;; cider-inspector.el --- Object inspector -*- lexical-binding: t -*- 2 3 ;; Copyright © 2013-2014 Vital Reactor, LLC 4 ;; Copyright © 2014-2023 Bozhidar Batsov and CIDER contributors 5 6 ;; Author: Ian Eslick <ian@vitalreactor.com> 7 ;; Bozhidar Batsov <bozhidar@batsov.dev> 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 <http://www.gnu.org/licenses/>. 21 22 ;; This file is not part of GNU Emacs. 23 24 ;;; Commentary: 25 26 ;; Clojure object inspector inspired by SLIME. 27 28 ;;; Code: 29 30 (require 'cl-lib) 31 (require 'easymenu) 32 (require 'seq) 33 (require 'cider-eval) 34 35 ;; =================================== 36 ;; Inspector Key Map and Derived Mode 37 ;; =================================== 38 39 (defconst cider-inspector-buffer "*cider-inspect*") 40 41 ;;; Customization 42 (defgroup cider-inspector nil 43 "Presentation and behavior of the CIDER value inspector." 44 :prefix "cider-inspector-" 45 :group 'cider 46 :package-version '(cider . "0.10.0")) 47 48 (defcustom cider-inspector-page-size 32 49 "Default page size in paginated inspector view. 50 The page size can be also changed interactively within the inspector." 51 :type '(integer :tag "Page size" 32) 52 :package-version '(cider . "0.10.0")) 53 54 (defcustom cider-inspector-max-atom-length 150 55 "Default max length of nested atoms before they are truncated. 56 'Atom' here means any collection member that satisfies (complement coll?). 57 The max length can be also changed interactively within the inspector." 58 :type '(integer :tag "Max atom length" 150) 59 :package-version '(cider . "1.1.0")) 60 61 (defcustom cider-inspector-max-coll-size 5 62 "Default number of nested collection members to display before truncating. 63 The max size can be also changed interactively within the inspector." 64 :type '(integer :tag "Max collection size" 5) 65 :package-version '(cider . "1.1.0")) 66 67 (defcustom cider-inspector-fill-frame nil 68 "Controls whether the CIDER inspector window fills its frame." 69 :type 'boolean 70 :package-version '(cider . "0.15.0")) 71 72 (defcustom cider-inspector-skip-uninteresting t 73 "Controls whether to skip over uninteresting values in the inspector. 74 Only applies to navigation with `cider-inspector-prev-inspectable-object' 75 and `cider-inspector-next-inspectable-object', values are still inspectable 76 by clicking or navigating to them by other means." 77 :type 'boolean 78 :package-version '(cider . "0.25.0")) 79 80 (defcustom cider-inspector-auto-select-buffer t 81 "Determines if the inspector buffer should be auto selected." 82 :type 'boolean 83 :package-version '(cider . "0.27.0")) 84 85 (defvar cider-inspector-uninteresting-regexp 86 (concat "nil" ; nils are not interesting 87 "\\|:" clojure--sym-regexp ; nor keywords 88 ;; FIXME: This range also matches ",", is it on purpose? 89 "\\|[+-.0-9]+") ; nor numbers. Note: BigInts, ratios etc. are interesting 90 "Regexp of uninteresting and skippable values.") 91 92 (defvar cider-inspector-mode-map 93 (let ((map (make-sparse-keymap))) 94 (set-keymap-parent map cider-popup-buffer-mode-map) 95 (define-key map (kbd "RET") #'cider-inspector-operate-on-point) 96 (define-key map [mouse-1] #'cider-inspector-operate-on-click) 97 (define-key map "l" #'cider-inspector-pop) 98 (define-key map "g" #'cider-inspector-refresh) 99 ;; Page-up/down 100 (define-key map [next] #'cider-inspector-next-page) 101 (define-key map [prior] #'cider-inspector-prev-page) 102 (define-key map " " #'cider-inspector-next-page) 103 (define-key map (kbd "M-SPC") #'cider-inspector-prev-page) 104 (define-key map (kbd "S-SPC") #'cider-inspector-prev-page) 105 (define-key map "s" #'cider-inspector-set-page-size) 106 (define-key map "a" #'cider-inspector-set-max-atom-length) 107 (define-key map "c" #'cider-inspector-set-max-coll-size) 108 (define-key map "d" #'cider-inspector-def-current-val) 109 (define-key map [tab] #'cider-inspector-next-inspectable-object) 110 (define-key map "\C-i" #'cider-inspector-next-inspectable-object) 111 (define-key map "n" #'cider-inspector-next-inspectable-object) 112 (define-key map [(shift tab)] #'cider-inspector-previous-inspectable-object) 113 (define-key map "p" #'cider-inspector-previous-inspectable-object) 114 (define-key map "f" #'forward-char) 115 (define-key map "b" #'backward-char) 116 ;; Emacs translates S-TAB to BACKTAB on X. 117 (define-key map [backtab] #'cider-inspector-previous-inspectable-object) 118 (easy-menu-define cider-inspector-mode-menu map 119 "Menu for CIDER's inspector." 120 `("CIDER Inspector" 121 ["Inspect" cider-inspector-operate-on-point] 122 ["Pop" cider-inspector-pop] 123 ["Refresh" cider-inspector-refresh] 124 "--" 125 ["Next Inspectable Object" cider-inspector-next-inspectable-object] 126 ["Previous Inspectable Object" cider-inspector-previous-inspectable-object] 127 "--" 128 ["Next Page" cider-inspector-next-page] 129 ["Previous Page" cider-inspector-prev-page] 130 ["Set Page Size" cider-inspector-set-page-size] 131 ["Set Max Atom Length" cider-inspector-set-max-atom-length] 132 ["Set Max Collection Size" cider-inspector-set-max-coll-size] 133 ["Define Var" cider-inspector-def-current-val] 134 "--" 135 ["Quit" cider-popup-buffer-quit-function] 136 )) 137 map)) 138 139 (define-derived-mode cider-inspector-mode special-mode "Inspector" 140 "Major mode for inspecting Clojure data structures. 141 142 \\{cider-inspector-mode-map}" 143 (set-syntax-table clojure-mode-syntax-table) 144 (setq-local electric-indent-chars nil) 145 (setq-local sesman-system 'CIDER) 146 (visual-line-mode 1)) 147 148 ;;;###autoload 149 (defun cider-inspect-last-sexp () 150 "Inspect the result of the the expression preceding point." 151 (interactive) 152 (cider-inspect-expr (cider-last-sexp) (cider-current-ns))) 153 154 ;;;###autoload 155 (defun cider-inspect-defun-at-point () 156 "Inspect the result of the \"top-level\" expression at point." 157 (interactive) 158 (cider-inspect-expr (cider-defun-at-point) (cider-current-ns))) 159 160 ;;;###autoload 161 (defun cider-inspect-last-result () 162 "Inspect the most recent eval result." 163 (interactive) 164 (cider-inspect-expr "*1" (cider-current-ns))) 165 166 ;;;###autoload 167 (defun cider-inspect (&optional arg) 168 "Inspect the result of the preceding sexp. 169 170 With a prefix argument ARG it inspects the result of the \"top-level\" form. 171 With a second prefix argument it prompts for an expression to eval and inspect." 172 (interactive "p") 173 (pcase arg 174 (1 (cider-inspect-last-sexp)) 175 (4 (cider-inspect-defun-at-point)) 176 (16 (call-interactively #'cider-inspect-expr)))) 177 178 (defvar cider-inspector-location-stack nil 179 "A stack used to save point locations in inspector buffers. 180 These locations are used to emulate `save-excursion' between 181 `cider-inspector-push' and `cider-inspector-pop' operations.") 182 183 (defvar cider-inspector-page-location-stack nil 184 "A stack used to save point locations in inspector buffers. 185 These locations are used to emulate `save-excursion' between 186 `cider-inspector-next-page' and `cider-inspector-prev-page' operations.") 187 188 (defvar cider-inspector-last-command nil 189 "Contains the value of the most recently used `cider-inspector-*' command. 190 This is used as an alternative to the built-in `last-command'. Whenever we 191 invoke any command through \\[execute-extended-command] and its variants, 192 the value of `last-command' is not set to the command it invokes.") 193 194 (defvar cider-inspector--current-repl nil 195 "Contains the reference to the REPL where inspector was last invoked from. 196 This is needed for internal inspector buffer operations (push, 197 pop) to execute against the correct REPL session.") 198 199 ;; Operations 200 ;;;###autoload 201 (defun cider-inspect-expr (expr ns) 202 "Evaluate EXPR in NS and inspect its value. 203 Interactively, EXPR is read from the minibuffer, and NS the 204 current buffer's namespace." 205 (interactive (list (cider-read-from-minibuffer "Inspect expression: " (cider-sexp-at-point)) 206 (cider-current-ns))) 207 (setq cider-inspector--current-repl (cider-current-repl)) 208 (when-let* ((value (cider-sync-request:inspect-expr 209 expr ns 210 cider-inspector-page-size 211 cider-inspector-max-atom-length 212 cider-inspector-max-coll-size))) 213 (cider-inspector--render-value value))) 214 215 (defun cider-inspector-pop () 216 "Pop the last value off the inspector stack and render it. 217 See `cider-sync-request:inspect-pop' and `cider-inspector--render-value'." 218 (interactive) 219 (setq cider-inspector-last-command 'cider-inspector-pop) 220 (when-let* ((value (cider-sync-request:inspect-pop))) 221 (cider-inspector--render-value value))) 222 223 (defun cider-inspector-push (idx) 224 "Inspect the value at IDX in the inspector stack and render it. 225 See `cider-sync-request:inspect-push' and `cider-inspector--render-value'" 226 (push (point) cider-inspector-location-stack) 227 (when-let* ((value (cider-sync-request:inspect-push idx))) 228 (cider-inspector--render-value value) 229 (cider-inspector-next-inspectable-object 1))) 230 231 (defun cider-inspector-refresh () 232 "Re-render the currently inspected value. 233 See `cider-sync-request:inspect-refresh' and `cider-inspector--render-value'" 234 (interactive) 235 (when-let* ((value (cider-sync-request:inspect-refresh))) 236 (cider-inspector--render-value value))) 237 238 (defun cider-inspector-next-page () 239 "Jump to the next page when inspecting a paginated sequence/map. 240 241 Does nothing if already on the last page." 242 (interactive) 243 (push (point) cider-inspector-page-location-stack) 244 (when-let* ((value (cider-sync-request:inspect-next-page))) 245 (cider-inspector--render-value value))) 246 247 (defun cider-inspector-prev-page () 248 "Jump to the previous page when expecting a paginated sequence/map. 249 250 Does nothing if already on the first page." 251 (interactive) 252 (setq cider-inspector-last-command 'cider-inspector-prev-page) 253 (when-let* ((value (cider-sync-request:inspect-prev-page))) 254 (cider-inspector--render-value value))) 255 256 (defun cider-inspector-set-page-size (page-size) 257 "Set the page size in pagination mode to the specified PAGE-SIZE. 258 259 Current page will be reset to zero." 260 (interactive (list (read-number "Page size: " cider-inspector-page-size))) 261 (when-let ((value (cider-sync-request:inspect-set-page-size page-size))) 262 (cider-inspector--render-value value))) 263 264 (defun cider-inspector-set-max-atom-length (max-length) 265 "Set the max length of nested atoms to MAX-LENGTH." 266 (interactive (list (read-number "Max atom length: " cider-inspector-max-atom-length))) 267 (when-let ((value (cider-sync-request:inspect-set-max-atom-length max-length))) 268 (cider-inspector--render-value value))) 269 270 (defun cider-inspector-set-max-coll-size (max-size) 271 "Set the number of nested collection members to display before truncating. 272 MAX-SIZE is the new value." 273 (interactive (list (read-number "Max collection size: " cider-inspector-max-coll-size))) 274 (when-let ((value (cider-sync-request:inspect-set-max-coll-size max-size))) 275 (cider-inspector--render-value value))) 276 277 (defun cider-inspector-def-current-val (var-name ns) 278 "Defines a var with VAR-NAME in current namespace. 279 280 Doesn't modify current page. When called interactively NS defaults to 281 current-namespace." 282 (interactive (let ((ns (cider-current-ns))) 283 (list (read-from-minibuffer (concat "Var name: " ns "/")) 284 ns))) 285 (setq cider-inspector--current-repl (cider-current-repl)) 286 (when-let* ((value (cider-sync-request:inspect-def-current-val ns var-name))) 287 (cider-inspector--render-value value) 288 (message "%s#'%s/%s = %s" cider-eval-result-prefix ns var-name value))) 289 290 ;; nREPL interactions 291 (defun cider-sync-request:inspect-pop () 292 "Move one level up in the inspector stack." 293 (thread-first '("op" "inspect-pop") 294 (cider-nrepl-send-sync-request cider-inspector--current-repl) 295 (nrepl-dict-get "value"))) 296 297 (defun cider-sync-request:inspect-push (idx) 298 "Inspect the inside value specified by IDX." 299 (thread-first `("op" "inspect-push" 300 "idx" ,idx) 301 (cider-nrepl-send-sync-request cider-inspector--current-repl) 302 (nrepl-dict-get "value"))) 303 304 (defun cider-sync-request:inspect-refresh () 305 "Re-render the currently inspected value." 306 (thread-first '("op" "inspect-refresh") 307 (cider-nrepl-send-sync-request cider-inspector--current-repl) 308 (nrepl-dict-get "value"))) 309 310 (defun cider-sync-request:inspect-next-page () 311 "Jump to the next page in paginated collection view." 312 (thread-first '("op" "inspect-next-page") 313 (cider-nrepl-send-sync-request cider-inspector--current-repl) 314 (nrepl-dict-get "value"))) 315 316 (defun cider-sync-request:inspect-prev-page () 317 "Jump to the previous page in paginated collection view." 318 (thread-first '("op" "inspect-prev-page") 319 (cider-nrepl-send-sync-request cider-inspector--current-repl) 320 (nrepl-dict-get "value"))) 321 322 (defun cider-sync-request:inspect-set-page-size (page-size) 323 "Set the page size in paginated view to PAGE-SIZE." 324 (thread-first `("op" "inspect-set-page-size" 325 "page-size" ,page-size) 326 (cider-nrepl-send-sync-request cider-inspector--current-repl) 327 (nrepl-dict-get "value"))) 328 329 (defun cider-sync-request:inspect-set-max-atom-length (max-length) 330 "Set the max length of nested atoms to MAX-LENGTH." 331 (thread-first `("op" "inspect-set-max-atom-length" 332 "max-atom-length" ,max-length) 333 (cider-nrepl-send-sync-request cider-inspector--current-repl) 334 (nrepl-dict-get "value"))) 335 336 (defun cider-sync-request:inspect-set-max-coll-size (max-size) 337 "Set the number of nested collection members to display before truncating. 338 MAX-SIZE is the new value." 339 (thread-first `("op" "inspect-set-max-coll-size" 340 "max-coll-size" ,max-size) 341 (cider-nrepl-send-sync-request cider-inspector--current-repl) 342 (nrepl-dict-get "value"))) 343 344 (defun cider-sync-request:inspect-def-current-val (ns var-name) 345 "Defines a var with VAR-NAME in NS with the current inspector value." 346 (thread-first `("op" "inspect-def-current-value" 347 "ns" ,ns 348 "var-name" ,var-name) 349 (cider-nrepl-send-sync-request cider-inspector--current-repl) 350 (nrepl-dict-get "value"))) 351 352 (defun cider-sync-request:inspect-expr (expr ns page-size max-atom-length max-coll-size) 353 "Evaluate EXPR in context of NS and inspect its result. 354 Set the page size in paginated view to PAGE-SIZE, maximum length of atomic 355 collection members to MAX-ATOM-LENGTH, and maximum size of nested collections to 356 MAX-COLL-SIZE if non nil." 357 (thread-first (append (nrepl--eval-request expr ns) 358 `("inspect" "true" 359 ,@(when page-size 360 `("page-size" ,page-size)) 361 ,@(when max-atom-length 362 `("max-atom-length" ,max-atom-length)) 363 ,@(when max-coll-size 364 `("max-coll-size" ,max-coll-size)))) 365 (cider-nrepl-send-sync-request cider-inspector--current-repl) 366 (nrepl-dict-get "value"))) 367 368 ;; Render Inspector from Structured Values 369 (defun cider-inspector--render-value (value) 370 "Render VALUE." 371 (cider-make-popup-buffer cider-inspector-buffer 'cider-inspector-mode 'ancillary) 372 (cider-inspector-render cider-inspector-buffer value) 373 (cider-popup-buffer-display cider-inspector-buffer cider-inspector-auto-select-buffer) 374 (when cider-inspector-fill-frame (delete-other-windows)) 375 (ignore-errors (cider-inspector-next-inspectable-object 1)) 376 (with-current-buffer cider-inspector-buffer 377 (when (eq cider-inspector-last-command 'cider-inspector-pop) 378 (setq cider-inspector-last-command nil) 379 ;; Prevents error message being displayed when we try to pop 380 ;; from the top-level of a data structure 381 (when cider-inspector-location-stack 382 (goto-char (pop cider-inspector-location-stack)))) 383 384 (when (eq cider-inspector-last-command 'cider-inspector-prev-page) 385 (setq cider-inspector-last-command nil) 386 ;; Prevents error message being displayed when we try to 387 ;; go to a prev-page from the first page 388 (when cider-inspector-page-location-stack 389 (goto-char (pop cider-inspector-page-location-stack)))))) 390 391 (defun cider-inspector-render (buffer str) 392 "Render STR in BUFFER." 393 (with-current-buffer buffer 394 (cider-inspector-mode) 395 (let ((inhibit-read-only t)) 396 (condition-case nil 397 (cider-inspector-render* (car (read-from-string str))) 398 (error (insert "\nInspector error for: " str)))) 399 (goto-char (point-min)))) 400 401 (defun cider-inspector-render* (elements) 402 "Render ELEMENTS." 403 (dolist (el elements) 404 (cider-inspector-render-el* el))) 405 406 (defun cider-inspector-render-el* (el) 407 "Render EL." 408 (cond ((symbolp el) (insert (symbol-name el))) 409 ((stringp el) (insert (propertize el 'font-lock-face 'font-lock-keyword-face))) 410 ((and (consp el) (eq (car el) :newline)) 411 (insert "\n")) 412 ((and (consp el) (eq (car el) :value)) 413 (cider-inspector-render-value (cadr el) (cl-caddr el))) 414 (t (message "Unrecognized inspector object: %s" el)))) 415 416 (defun cider-inspector-render-value (value idx) 417 "Render VALUE at IDX." 418 (cider-propertize-region 419 (list 'cider-value-idx idx 420 'mouse-face 'highlight) 421 (cider-inspector-render-el* (cider-font-lock-as-clojure value)))) 422 423 424 ;; =================================================== 425 ;; Inspector Navigation (lifted from SLIME inspector) 426 ;; =================================================== 427 428 (defun cider-find-inspectable-object (direction limit) 429 "Find the next/previous inspectable object. 430 DIRECTION can be either 'next or 'prev. 431 LIMIT is the maximum or minimum position in the current buffer. 432 433 Return a list of two values: If an object could be found, the 434 starting position of the found object and T is returned; 435 otherwise LIMIT and NIL is returned." 436 (let ((finder (cl-ecase direction 437 (next 'next-single-property-change) 438 (prev 'previous-single-property-change)))) 439 (let ((prop nil) (curpos (point))) 440 (while (and (not prop) (not (= curpos limit))) 441 (let ((newpos (funcall finder curpos 'cider-value-idx nil limit))) 442 (setq prop (get-text-property newpos 'cider-value-idx)) 443 (setq curpos newpos))) 444 (list curpos (and prop t))))) 445 446 (defun cider-inspector-next-inspectable-object (arg) 447 "Move point to the next inspectable object. 448 With optional ARG, move across that many objects. 449 If ARG is negative, move backwards." 450 (interactive "p") 451 (let ((maxpos (point-max)) (minpos (point-min)) 452 (previously-wrapped-p nil)) 453 ;; Forward. 454 (while (> arg 0) 455 (seq-let (pos foundp) (cider-find-inspectable-object 'next maxpos) 456 (if foundp 457 (progn (goto-char pos) 458 (unless (and cider-inspector-skip-uninteresting 459 (looking-at-p cider-inspector-uninteresting-regexp)) 460 (setq arg (1- arg)) 461 (setq previously-wrapped-p nil))) 462 (if (not previously-wrapped-p) ; cycle detection 463 (progn (goto-char minpos) (setq previously-wrapped-p t)) 464 (error "No inspectable objects"))))) 465 ;; Backward. 466 (while (< arg 0) 467 (seq-let (pos foundp) (cider-find-inspectable-object 'prev minpos) 468 ;; CIDER-OPEN-INSPECTOR inserts the title of an inspector page 469 ;; as a presentation at the beginning of the buffer; skip 470 ;; that. (Notice how this problem can not arise in ``Forward.'') 471 (if (and foundp (/= pos minpos)) 472 (progn (goto-char pos) 473 (unless (and cider-inspector-skip-uninteresting 474 (looking-at-p cider-inspector-uninteresting-regexp)) 475 (setq arg (1+ arg)) 476 (setq previously-wrapped-p nil))) 477 (if (not previously-wrapped-p) ; cycle detection 478 (progn (goto-char maxpos) (setq previously-wrapped-p t)) 479 (error "No inspectable objects"))))))) 480 481 (defun cider-inspector-previous-inspectable-object (arg) 482 "Move point to the previous inspectable object. 483 With optional ARG, move across that many objects. 484 If ARG is negative, move forwards." 485 (interactive "p") 486 (cider-inspector-next-inspectable-object (- arg))) 487 488 (defun cider-inspector-property-at-point () 489 "Return property at point." 490 (let* ((properties '(cider-value-idx cider-range-button 491 cider-action-number)) 492 (find-property 493 (lambda (point) 494 (cl-loop for property in properties 495 for value = (get-text-property point property) 496 when value 497 return (list property value))))) 498 (or (funcall find-property (point)) 499 (funcall find-property (max (point-min) (1- (point))))))) 500 501 (defun cider-inspector-operate-on-point () 502 "Invoke the command for the text at point. 503 1. If point is on a value then recursively call the inspector on 504 that value. 505 2. If point is on an action then call that action. 506 3. If point is on a range-button fetch and insert the range." 507 (interactive) 508 (seq-let (property value) (cider-inspector-property-at-point) 509 (cl-case property 510 (cider-value-idx 511 (cider-inspector-push value)) 512 ;; TODO: range and action handlers 513 (t (error "No object at point"))))) 514 515 (defun cider-inspector-operate-on-click (event) 516 "Move to EVENT's position and operate the part." 517 (interactive "@e") 518 (let ((point (posn-point (event-end event)))) 519 (cond ((and point 520 (or (get-text-property point 'cider-value-idx))) 521 (goto-char point) 522 (cider-inspector-operate-on-point)) 523 (t 524 (error "No clickable part here"))))) 525 526 (provide 'cider-inspector) 527 528 ;;; cider-inspector.el ends here