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