A JavaScript development environment for Emacs https://indium.readthedocs.io
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

546 lines
19 KiB

  1. ;;; indium-debugger.el --- Indium debugger -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2016-2018 Nicolas Petton
  3. ;; Author: Nicolas Petton <nicolas@petton.fr>
  4. ;; Keywords: tools
  5. ;; This program is free software; you can redistribute it and/or modify
  6. ;; it under the terms of the GNU General Public License as published by
  7. ;; the Free Software Foundation, either version 3 of the License, or
  8. ;; (at your option) any later version.
  9. ;; This program is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;; - always evaluate on the current frame if any (check for inspection, etc.)
  17. ;;; Code:
  18. (require 'seq)
  19. (require 'map)
  20. (require 'thingatpt)
  21. (require 'easymenu)
  22. (require 'indium-structs)
  23. (require 'indium-inspector)
  24. (require 'indium-render)
  25. (require 'indium-debugger-locals)
  26. (require 'indium-debugger-litable)
  27. (defgroup indium-debugger nil
  28. "JavaScript debugger"
  29. :prefix "indium-debugger-"
  30. :group 'indium)
  31. (defcustom indium-debugger-major-mode
  32. #'js-mode
  33. "Major mode used in debugger buffers."
  34. :group 'indium-debugger
  35. :type 'function)
  36. (defcustom indium-debugger-blackbox-regexps nil
  37. "List of file path regexps to blackbox when debugging.
  38. Blackboxed scripts will be ignored (stepped out) when stepping in
  39. from the debugger."
  40. :type '(repeat string))
  41. (defcustom indium-debugger-inspect-when-eval nil
  42. "When non-nil, use inspect as a default eval when debugging."
  43. :type 'boolean)
  44. (defvar indium-debugger-current-frame nil
  45. "Currently selected frame in the debugger.")
  46. (defvar indium-debugger-frames nil
  47. "Call frames of the current debugger session.")
  48. ;; When stepping, the execution is first resumed. To avoid visual glitches with
  49. ;; the header being removed and added again, only hide the header after a timeout.
  50. (defvar indium-debugger--header-timer nil
  51. "Timer used to hide the debugger header.")
  52. (defvar indium-debugger--buffer-with-header nil
  53. "Buffer in which the header is displayed.")
  54. (defconst indium-debugger-fringe-arrow-string
  55. #("." 0 1 (display (left-fringe right-triangle)))
  56. "Used as an overlay's before-string prop to place a fringe arrow.")
  57. (defvar indium-debugger-mode-map
  58. (let ((map (make-sparse-keymap)))
  59. (define-key map " " #'indium-debugger-step-over)
  60. (define-key map (kbd "i") #'indium-debugger-step-into)
  61. (define-key map (kbd "o") #'indium-debugger-step-out)
  62. (define-key map (kbd "c") #'indium-debugger-resume)
  63. (define-key map (kbd "l") #'indium-debugger-locals)
  64. (define-key map (kbd "s") #'indium-debugger-stack-frames)
  65. (define-key map (kbd "q") #'indium-debugger-resume)
  66. (define-key map (kbd "h") #'indium-debugger-here)
  67. (define-key map (kbd "e") #'indium-debugger-evaluate)
  68. (define-key map (kbd "n") #'indium-debugger-next-frame)
  69. (define-key map (kbd "p") #'indium-debugger-previous-frame)
  70. (easy-menu-define indium-debugger-mode-menu map
  71. "Menu for Indium debugger"
  72. '("Indium Debugger"
  73. ["Resume" indium-debugger-resume]
  74. ["Step over" indium-debugger-step-over]
  75. ["Step into" indium-debugger-step-into]
  76. ["Step out" indium-debugger-step-out]
  77. ["Jump here" indium-debugger-here]
  78. "--"
  79. ["Inspect locals" indium-debugger-locals]
  80. ["Show stack" indium-debugger-stack-frames]
  81. "--"
  82. ["Evaluate" indium-debugger-evaluate]
  83. "--"
  84. ["Jump to the next frame" indium-debugger-next-frame]
  85. ["Jump to the previous frame" indium-debugger-previous-frame]))
  86. map))
  87. (define-minor-mode indium-debugger-mode
  88. "Minor mode for debugging JS scripts.
  89. \\{indium-debugger-mode-map}"
  90. :group 'indium
  91. :lighter " JS-debug"
  92. :keymap indium-debugger-mode-map)
  93. (defun indium-debugger-paused (frames reason &optional description)
  94. "Handle execution pause.
  95. Setup the debugging stack FRAMES when the execution has paused.
  96. Display REASON in the echo area with an help message.
  97. If DESCRIPTION is non-nil, display it in an overlay describing
  98. the exception."
  99. (indium-debugger-set-frames frames)
  100. (indium-debugger-select-frame (car frames))
  101. (when description
  102. (indium-debugger-litable-add-exception-overlay description))
  103. (indium-debugger--show-debug-header reason))
  104. (defun indium-debugger-resumed (&rest _args)
  105. "Handle resumed execution.
  106. Unset the debugging context and turn off indium-debugger-mode."
  107. (message "Execution resumed")
  108. (indium-debugger-unset-frames)
  109. (indium-debugger--hide-debug-header)
  110. (seq-doseq (buf (seq-filter (lambda (buf)
  111. (with-current-buffer buf
  112. indium-debugger-mode))
  113. (buffer-list)))
  114. (with-current-buffer buf
  115. (when overlay-arrow-position
  116. (set-marker overlay-arrow-position nil (current-buffer)))
  117. (indium-debugger-unset-current-buffer)
  118. (indium-debugger-litable-unset-buffer)))
  119. (let ((locals-buffer (indium-debugger-locals-get-buffer))
  120. (frames-buffer (indium-debugger-frames-get-buffer)))
  121. (when locals-buffer (kill-buffer locals-buffer))
  122. (when frames-buffer (kill-buffer frames-buffer))))
  123. (defun indium-debugger-next-frame ()
  124. "Jump to the next frame in the frame stack."
  125. (interactive)
  126. (indium-debugger--jump-to-frame 'forward))
  127. (defun indium-debugger-previous-frame ()
  128. "Jump to the previous frame in the frame stack."
  129. (interactive)
  130. (indium-debugger--jump-to-frame 'backward))
  131. (defun indium-debugger-stack-frames ()
  132. "List the stack frames in a separate buffer and switch to it."
  133. (interactive)
  134. (let ((buf (indium-debugger-frames-get-buffer-create))
  135. (inhibit-read-only t))
  136. (with-current-buffer buf
  137. (indium-debugger-frames-list indium-debugger-frames
  138. indium-debugger-current-frame))
  139. (pop-to-buffer buf)))
  140. (defun indium-debugger--jump-to-frame (direction)
  141. "Jump to the next frame in DIRECTION.
  142. DIRECTION is `forward' or `backward' (in the frame list)."
  143. (let* ((current-position (seq-position indium-debugger-frames
  144. indium-debugger-current-frame))
  145. (step (pcase direction
  146. (`forward -1)
  147. (`backward 1)))
  148. (position (+ current-position step)))
  149. (when (>= position (seq-length indium-debugger-frames))
  150. (user-error "End of frames"))
  151. (when (< position 0)
  152. (user-error "Beginning of frames"))
  153. (indium-debugger-select-frame (seq-elt indium-debugger-frames position))))
  154. (defun indium-debugger-select-frame (frame)
  155. "Make FRAME the current debugged stack frame.
  156. Setup a debugging buffer for the current stack FRAME and switch
  157. to that buffer.
  158. If no local file exists for the FRAME, ask the user if the remote
  159. source for that frame should be downloaded. If not, resume the
  160. execution."
  161. (setq indium-debugger-current-frame frame)
  162. (switch-to-buffer (indium-debugger-get-buffer-create))
  163. (if buffer-file-name
  164. (indium-debugger-setup-buffer-with-file)
  165. (progn
  166. (if (yes-or-no-p "No file found for debugging (sourcemap issue?), download script source (might be slow)?")
  167. (progn
  168. (message "Downloading script source for debugging...")
  169. (indium-client-get-frame-source
  170. frame
  171. (lambda (source)
  172. (with-current-buffer (indium-debugger-get-buffer-create)
  173. (indium-debugger-setup-buffer-with-source source))
  174. (message "Downloading script source for debugging...done!"))))
  175. (indium-client-resume)))))
  176. (defun indium-debugger-setup-buffer-with-file ()
  177. "Setup the current buffer for debugging."
  178. (when (buffer-modified-p)
  179. (revert-buffer nil nil t))
  180. (indium-debugger--goto-current-frame)
  181. (indium-debugger-litable-setup-buffer))
  182. (defun indium-debugger-setup-buffer-with-source (source)
  183. "Setup the current buffer with the frame SOURCE."
  184. (unless (string= (buffer-substring-no-properties (point-min) (point-max))
  185. source)
  186. (let ((inhibit-read-only t))
  187. (erase-buffer)
  188. (insert source)))
  189. (indium-debugger--goto-current-frame)
  190. (indium-debugger-litable-setup-buffer))
  191. (defun indium-debugger--goto-current-frame ()
  192. "Move the point to the current stack frame position in the current buffer."
  193. (let* ((location (indium-frame-location indium-debugger-current-frame)))
  194. (goto-char (point-min))
  195. (forward-line (1- (indium-location-line location)))
  196. (forward-char (indium-location-column location)))
  197. (indium-debugger-setup-overlay-arrow)
  198. (indium-debugger-highlight-node)
  199. (indium-debugger-locals-maybe-refresh)
  200. (indium-debugger-frames-maybe-refresh))
  201. (defun indium-debugger--show-debug-header (reason)
  202. "Display a help message with REASON in the header."
  203. (when indium-debugger--header-timer
  204. (cancel-timer indium-debugger--header-timer)
  205. (setq indium-debugger--header-timer nil))
  206. (let ((header (concat (propertize (or reason "")
  207. 'face 'font-lock-warning-face)
  208. " "
  209. (propertize "SPC"
  210. 'face 'font-lock-keyword-face)
  211. " over "
  212. (propertize "i"
  213. 'face 'font-lock-keyword-face)
  214. "nto "
  215. (propertize "o"
  216. 'face 'font-lock-keyword-face)
  217. "ut "
  218. (propertize "c"
  219. 'face 'font-lock-keyword-face)
  220. "ontinue "
  221. (propertize "h"
  222. 'face 'font-lock-keyword-face)
  223. "ere "
  224. (propertize "l"
  225. 'face 'font-lock-keyword-face)
  226. "ocals "
  227. (propertize "e"
  228. 'face 'font-lock-keyword-face)
  229. "val "
  230. (propertize "s"
  231. 'face 'font-lock-keyword-face)
  232. "tack "
  233. (propertize "n"
  234. 'face 'font-lock-keyword-face)
  235. "ext "
  236. (propertize "p"
  237. 'face 'font-lock-keyword-face)
  238. "rev")))
  239. (when (and indium-debugger--buffer-with-header
  240. (not (eq indium-debugger--buffer-with-header (current-buffer))))
  241. (with-current-buffer indium-debugger--buffer-with-header
  242. (setq header-line-format nil)))
  243. (setq indium-debugger--buffer-with-header (current-buffer))
  244. (setq header-line-format header)
  245. (force-mode-line-update)))
  246. (defun indium-debugger--hide-debug-header ()
  247. "Hide the debugger header."
  248. (setq indium-debugger--header-timer
  249. (run-at-time
  250. "0.3"
  251. nil
  252. (lambda ()
  253. (when indium-debugger--buffer-with-header
  254. (with-current-buffer indium-debugger--buffer-with-header
  255. (setq header-line-format nil)
  256. (setq indium-debugger--buffer-with-header nil)
  257. (force-mode-line-update)))))))
  258. (defun indium-debugger-setup-overlay-arrow ()
  259. "Setup the overlay pointing to the current debugging line."
  260. (let ((pos (line-beginning-position)))
  261. (setq overlay-arrow-string "=>")
  262. (setq overlay-arrow-position (make-marker))
  263. (set-marker overlay-arrow-position pos (current-buffer))))
  264. (defun indium-debugger-highlight-node ()
  265. "Highlight the current AST node where the execution has paused."
  266. (let ((beg (point))
  267. (end (line-end-position)))
  268. (indium-debugger-remove-highlights)
  269. (overlay-put (make-overlay beg end)
  270. 'face 'indium-highlight-face)))
  271. (defun indium-debugger-remove-highlights ()
  272. "Remove all debugging highlighting overlays from the current buffer."
  273. (remove-overlays (point-min) (point-max) 'face 'indium-highlight-face))
  274. (defun indium-debugger-top-frame ()
  275. "Return the top frame of the current debugging context."
  276. (car indium-debugger-frames))
  277. (defun indium-debugger-step-into ()
  278. "Request a step into."
  279. (interactive)
  280. (indium-client-step-into))
  281. (defun indium-debugger-step-over ()
  282. "Request a step over."
  283. (interactive)
  284. (indium-client-step-over))
  285. (defun indium-debugger-step-out ()
  286. "Request a step out."
  287. (interactive)
  288. (indium-client-step-out))
  289. (defun indium-debugger-resume ()
  290. "Request the runtime to resume the execution."
  291. (interactive)
  292. (indium-client-resume))
  293. (defun indium-debugger-here ()
  294. "Request the runtime to resume the execution until the point.
  295. When the position of the point is reached, pause the execution."
  296. (interactive)
  297. (indium-client-continue-to-location (indium-location-at-point)))
  298. (defun indium-debugger-switch-to-debugger-buffer ()
  299. "Switch to the debugger buffer.
  300. If there is no debugging session, signal an error."
  301. (unless indium-debugger-current-frame
  302. (user-error "No debugger to switch to"))
  303. (indium-debugger-select-frame indium-debugger-current-frame))
  304. (defun indium-debugger-evaluate (expression &optional frame)
  305. "Prompt for EXPRESSION to be evaluated in the context of FRAME.
  306. When called interactively, FRAME is the current frame.
  307. When called with a prefix argument, or when
  308. `indium-debugger-inspect-when-eval' is non-nil, inspect the
  309. result of the evaluation if possible."
  310. (interactive (list
  311. (let ((default (if (region-active-p)
  312. (buffer-substring-no-properties (mark) (point))
  313. (thing-at-point 'symbol))))
  314. (read-string (format "Evaluate on frame: (%s): " default)
  315. nil nil default))
  316. indium-debugger-current-frame))
  317. (indium-client-evaluate expression
  318. frame
  319. (lambda (value)
  320. (let ((inspect (and (or indium-debugger-inspect-when-eval
  321. current-prefix-arg)
  322. (map-elt value 'objectid))))
  323. (if inspect
  324. (indium-inspector-inspect value)
  325. (message "%s" (indium-render-remote-object-to-string value)))))))
  326. ;; Debugging context
  327. (defun indium-debugger-set-frames (frames)
  328. "Set the debugger FRAMES."
  329. (setq indium-debugger-frames frames)
  330. (setq indium-debugger-current-frame (car frames)))
  331. (defun indium-debugger-unset-frames ()
  332. "Remove debugging information from the current connection."
  333. (setq indium-debugger-frames nil)
  334. (setq indium-debugger-current-frame nil))
  335. (defun indium-debugger-get-current-scopes ()
  336. "Return the scope of the current stack frame."
  337. (and indium-debugger-current-frame
  338. (indium-frame-scope-chain indium-debugger-current-frame)))
  339. (defun indium-debugger-get-scopes-properties (scopes callback)
  340. "Request a list of all properties in SCOPES.
  341. CALLBACK is evaluated with the result."
  342. (seq-do (lambda (scope)
  343. (indium-debugger-get-scope-properties scope callback))
  344. scopes))
  345. (defun indium-debugger-get-scope-properties (scope callback)
  346. "Request the properties of SCOPE and evaluate CALLBACK.
  347. CALLBACK is evaluated with two arguments, the properties and SCOPE."
  348. (let-alist scope
  349. (indium-client-get-properties (indium-scope-id scope)
  350. (lambda (properties)
  351. (funcall callback properties scope)))))
  352. (defun indium-debugger-get-buffer-create ()
  353. "Create a debugger buffer for the current connection and return it.
  354. If a buffer already exists, just return it."
  355. (let* ((location (indium-frame-location indium-debugger-current-frame))
  356. (file (indium-location-file location))
  357. (buf (if (and file (file-regular-p file))
  358. (find-file file)
  359. (get-buffer-create (indium-debugger--buffer-name-no-file)))))
  360. (indium-debugger-setup-buffer buf)
  361. buf))
  362. (defun indium-debugger--buffer-name-no-file ()
  363. "Return the name of a debugger buffer.
  364. This name should used when no local file can be found for a stack
  365. frame."
  366. "*JS Debugger*")
  367. (defun indium-debugger-setup-buffer (buffer)
  368. "Setup BUFFER for debugging."
  369. (with-current-buffer buffer
  370. (unless (or buffer-file-name
  371. (eq major-mode indium-debugger-major-mode))
  372. (funcall indium-debugger-major-mode))
  373. (indium-debugger-mode 1)
  374. (when (derived-mode-p 'js2-mode)
  375. (js2-reparse))
  376. (read-only-mode)))
  377. (defun indium-debugger-unset-current-buffer ()
  378. "Unset `indium-debugger-mode from the current buffer'."
  379. (indium-debugger-remove-highlights)
  380. (when overlay-arrow-position
  381. (set-marker overlay-arrow-position nil (current-buffer)))
  382. (indium-debugger-mode -1)
  383. (read-only-mode -1)
  384. (indium-debugger-litable-unset-buffer))
  385. ;; Frame listing
  386. (defun indium-debugger-frames-maybe-refresh ()
  387. "When a buffer listing the stack frames is open, refresh it."
  388. (interactive)
  389. (let ((buf (indium-debugger-frames-get-buffer))
  390. (inhibit-read-only t))
  391. (when buf
  392. (with-current-buffer buf
  393. (indium-debugger-frames-list indium-debugger-frames
  394. indium-debugger-current-frame)))))
  395. (defun indium-debugger-frames-list (frames &optional current-frame)
  396. "Render the list of stack frames FRAMES.
  397. CURRENT-FRAME is the current stack frame in the debugger."
  398. (save-excursion
  399. (erase-buffer)
  400. (indium-render-header "Debugger stack")
  401. (newline 2)
  402. (seq-doseq (frame frames)
  403. (indium-render-frame
  404. frame
  405. (eq current-frame frame))
  406. (newline))))
  407. (defun indium-debugger-frames-select-frame (frame)
  408. "Select FRAME and switch to the corresponding debugger buffer."
  409. (interactive)
  410. (indium-debugger-select-frame frame))
  411. (defun indium-debugger-frames-next-frame ()
  412. "Go to the next frame in the stack."
  413. (interactive)
  414. (indium-debugger-frames-goto-next 'next))
  415. (defun indium-debugger-frames-previous-frame ()
  416. "Go to the previous frame in the stack."
  417. (interactive)
  418. (indium-debugger-frames-goto-next 'previous))
  419. (defun indium-debugger-frames-goto-next (direction)
  420. "Go to the next frame in DIRECTION."
  421. (let ((next (eq direction 'next)))
  422. (forward-line (if next 1 -1))
  423. (back-to-indentation)
  424. (while (and (not (if next
  425. (eobp)
  426. (bobp)))
  427. (not (get-text-property (point) 'indium-action)))
  428. (forward-char (if next 1 -1)))))
  429. (defun indium-debugger-frames-get-buffer ()
  430. "Return the buffer listing frames for the current connection.
  431. If no buffer is found, return nil."
  432. (get-buffer (indium-debugger-frames-buffer-name)))
  433. (defun indium-debugger-frames-buffer-name ()
  434. "Return the name of the frames buffer for the current connection."
  435. "*JS Frames*")
  436. (defun indium-debugger-frames-get-buffer-create ()
  437. "Create a buffer for listing frames unless one exists, and return it."
  438. (let ((buf (indium-debugger-frames-get-buffer)))
  439. (unless buf
  440. (setq buf (generate-new-buffer (indium-debugger-frames-buffer-name)))
  441. (indium-debugger-frames-setup-buffer buf))
  442. buf))
  443. (defun indium-debugger-frames-setup-buffer (buffer)
  444. "Setup the frames BUFFER."
  445. (with-current-buffer buffer
  446. (indium-debugger-frames-mode)
  447. (setq-local truncate-lines nil)))
  448. (defvar indium-debugger-frames-mode-map
  449. (let ((map (make-sparse-keymap)))
  450. (define-key map [return] #'indium-follow-link)
  451. (define-key map (kbd "C-m") #'indium-follow-link)
  452. (define-key map (kbd "n") #'indium-debugger-frames-next-frame)
  453. (define-key map (kbd "p") #'indium-debugger-frames-previous-frame)
  454. (define-key map [tab] #'indium-debugger-frames-next-frame)
  455. (define-key map [backtab] #'indium-debugger-frames-previous-frame)
  456. map))
  457. (define-derived-mode indium-debugger-frames-mode special-mode "Frames"
  458. "Major mode visualizind and navigating the JS stack.
  459. \\{indium-debugger-frames--mode-map}"
  460. (setq buffer-read-only t)
  461. (font-lock-ensure)
  462. (read-only-mode))
  463. (add-hook 'indium-client-debugger-paused-hook #'indium-debugger-paused)
  464. (add-hook 'indium-client-debugger-resumed-hook #'indium-debugger-resumed)
  465. (provide 'indium-debugger)
  466. ;;; indium-debugger.el ends here