forked from svaante/dape
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dape.el
5450 lines (5017 loc) · 230 KB
/
dape.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
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; dape.el --- Debug Adapter Protocol for Emacs -*- lexical-binding: t -*-
;; Copyright (C) 2023 Free Software Foundation, Inc.
;; Author: Daniel Pettersson
;; Maintainer: Daniel Pettersson <daniel@dpettersson.net>
;; Created: 2023
;; License: GPL-3.0-or-later
;; Version: 0.16.0
;; Homepage: https://github.com/svaante/dape
;; Package-Requires: ((emacs "29.1") (jsonrpc "1.0.25"))
;; This file is not part of GNU Emacs.
;; 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:
;; Dape is a debug adapter client for Emacs. The debug adapter
;; protocol, much like its more well-known counterpart, the language
;; server protocol, aims to establish a common API for programming
;; tools. However, instead of functionalities such as code
;; completions, it provides a standardized interface for debuggers.
;; To begin a debugging session, invoke the `dape' command. In the
;; minibuffer prompt, enter a debug adapter configuration name from
;; `dape-configs'.
;; For complete functionality, make sure to enable `eldoc-mode' in your
;; source buffers and `repeat-mode' for more pleasant key mappings.
;; Package looks is heavily inspired by gdb-mi.el
;;; Code:
(require 'cl-lib)
(require 'subr-x)
(require 'seq)
(require 'font-lock)
(require 'pulse)
(require 'comint)
(require 'repeat)
(require 'compile)
(require 'tree-widget)
(require 'project)
(require 'gdb-mi)
(require 'hexl)
(require 'tramp)
(require 'jsonrpc)
(require 'eglot) ;; jdtls config
;;; Obsolete aliases
(define-obsolete-variable-alias 'dape-buffer-window-arrangment 'dape-buffer-window-arrangement "0.3.0")
(define-obsolete-variable-alias 'dape-read-memory-default-count 'dape-memory-page-size "0.8.0")
(define-obsolete-variable-alias 'dape-on-start-hooks 'dape-start-hook "0.13.0")
(define-obsolete-variable-alias 'dape-on-stopped-hooks 'dape-stopped-hook "0.13.0")
(define-obsolete-variable-alias 'dape-update-ui-hooks 'dape-update-ui-hook "0.13.0")
(define-obsolete-variable-alias 'dape-compile-compile-hooks 'dape-compile-hook "0.13.0")
;;; Forward declarations
(defvar hl-line-mode)
(defvar hl-line-sticky-flag)
(declare-function global-hl-line-highlight "hl-line" ())
(declare-function hl-line-highlight "hl-line" ())
;;; Custom
(defgroup dape nil
"Debug Adapter Protocol for Emacs."
:prefix "dape-"
:group 'applications)
(defcustom dape-adapter-dir
(file-name-as-directory (concat user-emacs-directory "debug-adapters"))
"Directory to store downloaded adapters in."
:type 'string)
(defcustom dape-configs
`((attach
modes nil
ensure (lambda (config)
(unless (plist-get config 'port)
(user-error "Missing `port' property")))
host "localhost"
:request "attach")
(launch
modes nil
command-cwd dape-command-cwd
ensure (lambda (config)
(unless (plist-get config 'command)
(user-error "Missing `command' property")))
:request "launch")
,(let* ((extension-directory
(expand-file-name
(file-name-concat dape-adapter-dir "bash-debug" "extension")))
(bashdb-dir (file-name-concat extension-directory "bashdb_dir")))
`(bash-debug
modes (sh-mode bash-ts-mode)
ensure (lambda (config)
(dape-ensure-command config)
(let ((dap-debug-server-path
(car (plist-get config 'command-args))))
(unless (file-exists-p dap-debug-server-path)
(user-error "File %S does not exist" dap-debug-server-path))))
command "node"
command-args (,(file-name-concat extension-directory "out" "bashDebug.js"))
fn (lambda (config)
(thread-first config
(plist-put :pathBashdbLib ,bashdb-dir)
(plist-put :pathBashdb (file-name-concat ,bashdb-dir "bashdb"))
(plist-put :env `(:BASHDB_HOME ,,bashdb-dir . ,(plist-get config :env)))))
:type "bashdb"
:cwd dape-cwd
:program dape-buffer-default
:args []
:pathBash "bash"
:pathCat "cat"
:pathMkfifo "mkfifo"
:pathPkill "pkill"))
,@(let ((codelldb
`( ensure dape-ensure-command
command-cwd dape-command-cwd
command ,(file-name-concat dape-adapter-dir
"codelldb"
"extension"
"adapter"
"codelldb")
port :autoport
:type "lldb"
:request "launch"
:cwd "."))
(common `(:args [] :stopOnEntry nil)))
`((codelldb-cc
modes (c-mode c-ts-mode c++-mode c++-ts-mode)
command-args ("--port" :autoport)
,@codelldb
:program "a.out"
,@common)
(codelldb-rust
modes (rust-mode rust-ts-mode)
command-args ("--port" :autoport
"--settings" "{\"sourceLanguages\":[\"rust\"]}")
,@codelldb
:program (lambda ()
(file-name-concat "target" "debug"
(thread-first (dape-cwd)
(directory-file-name)
(file-name-split)
(last)
(car))))
,@common)))
(cpptools
modes (c-mode c-ts-mode c++-mode c++-ts-mode)
ensure dape-ensure-command
command-cwd dape-command-cwd
command ,(file-name-concat dape-adapter-dir
"cpptools"
"extension"
"debugAdapters"
"bin"
"OpenDebugAD7")
fn (lambda (config)
;; For MI=GDB the :program path need to be absolute
(let ((program (plist-get config :program)))
(if (file-name-absolute-p program)
config
(thread-last (tramp-file-local-name (dape--guess-root config))
(expand-file-name program)
(plist-put config :program)))))
:type "cppdbg"
:request "launch"
:cwd "."
:program "a.out"
:MIMode ,(seq-find 'executable-find '("lldb" "gdb")))
,@(let ((debugpy
`( modes (python-mode python-ts-mode)
ensure (lambda (config)
(dape-ensure-command config)
(let ((python (dape-config-get config 'command)))
(unless (zerop
(call-process-shell-command
(format "%s -c \"import debugpy.adapter\"" python)))
(user-error "%s module debugpy is not installed" python))))
command "python"
command-args ("-m" "debugpy.adapter" "--host" "0.0.0.0" "--port" :autoport)
port :autoport
:request "launch"
:type "python"
:cwd dape-cwd))
(common
`( :args []
:justMyCode nil
:console "integratedTerminal"
:showReturnValue t
:stopOnEntry nil)))
`((debugpy ,@debugpy
:program dape-buffer-default
,@common)
(debugpy-module ,@debugpy
:module (lambda ()
(thread-first default-directory
(directory-file-name)
(file-name-split)
(last)
(car)))
,@common)))
(dlv
modes (go-mode go-ts-mode)
ensure dape-ensure-command
command "dlv"
command-args ("dap" "--listen" "127.0.0.1::autoport")
command-cwd dape-command-cwd
command-insert-stderr t
port :autoport
:request "launch"
:type "debug"
:cwd "."
:program ".")
(flutter
ensure dape-ensure-command
modes (dart-mode)
command "flutter"
command-args ("debug_adapter")
command-cwd dape-command-cwd
:type "dart"
:cwd "."
:program "lib/main.dart"
:toolArgs ["-d" "all"])
(gdb
ensure (lambda (config)
(dape-ensure-command config)
(let* ((default-directory
(or (dape-config-get config 'command-cwd)
default-directory))
(command (dape-config-get config 'command))
(output (shell-command-to-string (format "%s --version" command)))
(version (save-match-data
(when (string-match "GNU gdb \\(?:(.*) \\)?\\([0-9.]+\\)" output)
(string-to-number (match-string 1 output))))))
(unless (>= version 14.1)
(user-error "Requires gdb version >= 14.1"))))
modes (c-mode c-ts-mode c++-mode c++-ts-mode)
command-cwd dape-command-cwd
command "gdb"
command-args ("--interpreter=dap")
defer-launch-attach t
:request "launch"
:program "a.out"
:args []
:stopAtBeginningOfMainSubprogram nil)
(godot
modes (gdscript-mode)
port 6006
:request "launch"
:type "server")
,@(let ((js-debug
`( ensure ,(lambda (config)
(dape-ensure-command config)
(when-let ((runtime-executable
(dape-config-get config :runtimeExecutable)))
(dape--ensure-executable runtime-executable))
(let ((dap-debug-server-path
(car (plist-get config 'command-args))))
(unless (file-exists-p dap-debug-server-path)
(user-error "File %S does not exist" dap-debug-server-path))))
command "node"
command-args (,(expand-file-name
(file-name-concat dape-adapter-dir
"js-debug"
"src"
"dapDebugServer.js"))
:autoport)
port :autoport)))
`((js-debug-node
modes (js-mode js-ts-mode)
,@js-debug
:type "pwa-node"
:cwd dape-cwd
:program dape-buffer-default
:console "internalConsole")
(js-debug-ts-node
modes (typescript-mode typescript-ts-mode)
,@js-debug
:type "pwa-node"
:runtimeExecutable "ts-node"
:cwd dape-cwd
:program dape-buffer-default
:console "internalConsole")
(js-debug-node-attach
modes (js-mode js-ts-mode typescript-mode typescript-ts-mode)
,@js-debug
:type "pwa-node"
:request "attach"
:port 9229)
(js-debug-chrome
modes (js-mode js-ts-mode typescript-mode typescript-ts-mode)
,@js-debug
:type "pwa-chrome"
:url "http://localhost:3000"
:webRoot dape-cwd)))
,@(let ((lldb-common
`( modes (c-mode c-ts-mode c++-mode c++-ts-mode rust-mode rust-ts-mode rustic-mode)
ensure dape-ensure-command
command-cwd dape-command-cwd
:cwd "."
:program "a.out")))
`((lldb-vscode
command "lldb-vscode"
:type "lldb-vscode"
,@lldb-common)
(lldb-dap
command "lldb-dap"
:type "lldb-dap"
,@lldb-common)))
(netcoredbg
modes (csharp-mode csharp-ts-mode)
ensure dape-ensure-command
command "netcoredbg"
command-args ["--interpreter=vscode"]
:request "launch"
:cwd dape-cwd
:program (lambda ()
(let ((dlls
(file-expand-wildcards
(file-name-concat "bin" "Debug" "*" "*.dll"))))
(if dlls
(file-relative-name
(file-relative-name (car dlls)))
".dll"
(dape-cwd))))
:stopAtEntry nil)
(ocamlearlybird
ensure dape-ensure-command
modes (tuareg-mode caml-mode)
command "ocamlearlybird"
command-args ("debug")
:type "ocaml"
:program (lambda ()
(file-name-concat
(dape-cwd)
"_build" "default" "bin"
(concat
(file-name-base (dape-buffer-default))
".bc")))
:console "internalConsole"
:stopOnEntry nil
:arguments [])
(rdbg
modes (ruby-mode ruby-ts-mode)
ensure dape-ensure-command
command "rdbg"
command-args ("-O" "--host" "0.0.0.0" "--port" :autoport "-c" "--" :-c)
fn (lambda (config)
(plist-put config 'command-args
(mapcar (lambda (arg)
(if (eq arg :-c)
(plist-get config '-c)
arg))
(plist-get config 'command-args))))
port :autoport
command-cwd dape-command-cwd
:type "Ruby"
;; -- examples:
;; rails server
;; bundle exec ruby foo.rb
;; bundle exec rake test
-c (lambda ()
(format "ruby %s"
(or (dape-buffer-default) ""))))
(jdtls
modes (java-mode java-ts-mode)
ensure (lambda (config)
(let ((file (dape-config-get config :filePath)))
(unless (and (stringp file) (file-exists-p file))
(user-error "Unable to find locate :filePath `%s'" file))
(with-current-buffer (find-file-noselect file)
(unless (eglot-current-server)
(user-error "No eglot instance active in buffer %s" (current-buffer)))
(unless (seq-contains-p (eglot--server-capable :executeCommandProvider :commands)
"vscode.java.resolveClasspath")
(user-error "Jdtls instance does not bundle java-debug-server, please install")))))
fn (lambda (config)
(with-current-buffer
(find-file-noselect (dape-config-get config :filePath))
(if-let ((server (eglot-current-server)))
(pcase-let ((`[,module-paths ,class-paths]
(eglot-execute-command server
"vscode.java.resolveClasspath"
(vector (plist-get config :mainClass)
(plist-get config :projectName))))
(port (eglot-execute-command server
"vscode.java.startDebugSession" nil)))
(thread-first config
(plist-put 'port port)
(plist-put :modulePaths module-paths)
(plist-put :classPaths class-paths)))
server)))
,@(cl-flet ((resolve-main-class (key)
(ignore-errors
(let* ((main-classes
(eglot-execute-command (eglot-current-server)
"vscode.java.resolveMainClass"
(file-name-nondirectory
(directory-file-name (dape-cwd)))))
(main-class
(or (seq-find (lambda(val)
(equal (plist-get val :filePath)
(buffer-file-name)))
main-classes)
(aref main-classes 0))))
(plist-get main-class key)))))
`(:filePath
,(lambda ()
(or (resolve-main-class :filePath)
(expand-file-name (dape-buffer-default) (dape-cwd))))
:mainClass
,(lambda ()
(or (resolve-main-class :mainClass) ""))
:projectName
,(lambda ()
(or (resolve-main-class :projectName) ""))))
:args ""
:stopOnEntry nil
:type "java"
:request "launch"
:vmArgs " -XX:+ShowCodeDetailsInExceptionMessages"
:console "integratedConsole"
:internalConsoleOptions "neverOpen")
(xdebug
modes (php-mode php-ts-mode)
ensure (lambda (config)
(dape-ensure-command config)
(let ((dap-debug-server-path
(car (plist-get config 'command-args))))
(unless (file-exists-p dap-debug-server-path)
(user-error "File %S does not exist" dap-debug-server-path))))
command "node"
command-args (,(expand-file-name
(file-name-concat dape-adapter-dir
"php-debug"
"extension"
"out"
"phpDebug.js")))
:type "php"
:port 9003))
"This variable holds the dape configurations as an alist.
In this alist, the car element serves as a symbol identifying each
configuration. Each configuration, in turn, is a property list (plist)
where keys can be symbols or keywords.
Symbol keys (Used by dape):
- fn: Function or list of functions, takes config and returns config.
If list functions are applied in order.
See `dape-default-config-functions'.
- ensure: Function to ensure that adapter is available.
- command: Shell command to initiate the debug adapter.
- command-args: List of string arguments for the command.
- command-cwd: Working directory for the command, if not supplied
`default-directory' will be used.
- command-env: Property list (plist) of environment variables to
set when running the command. Keys can be strings, symbols or
keywords.
- command-insert-stderr: If non nil treat stderr from adapter as
stderr output from debugged program.
- prefix-local: Path prefix for Emacs file access.
- prefix-remote: Path prefix for debugger file access.
- host: Host of the debug adapter.
- port: Port of the debug adapter.
- modes: List of modes where the configuration is active in `dape'
completions.
- compile: Executes a shell command with `dape-compile-fn'.
- defer-launch-attach: If launch/attach request should be sent
after initialize or configurationDone. If nil launch/attach are
sent after initialize request else it's sent after
configurationDone. This key exist to accommodate the two different
interpretations of the DAP specification.
See: GDB bug 32090.
Connection to Debug Adapter:
- If command is specified and not port, dape communicates with the
debug adapter through stdin/stdout.
- If host and port are specified, dape connects to the debug adapter.
If command is specified, dape waits until the command initializes
before connecting to host and port.
Keywords in configuration:
Keywords (symbols starting with colon) are transmitted to the
adapter during the initialize and launch/attach requests. Refer to
`json-serialize' for detailed information on how dape serializes
these keyword elements. Dape uses nil as false.
Functions and symbols:
- If a value is a function, its return value replaces the key's
value before execution. The function is called with no arguments.
- If a value is a symbol, it resolves recursively before execution."
:type '(alist :key-type (symbol :tag "Name")
:value-type
(plist :options
(((const :tag "List of modes where config is active in `dape' completions" modes) (repeat function))
((const :tag "Ensures adapter availability" ensure) function)
((const :tag "Transforms configuration at runtime" fn) (choice function (repeat function)))
((const :tag "Shell command to initiate the debug adapter" command) (choice string symbol))
((const :tag "List of string arguments for command" command-args) (repeat string))
((const :tag "List of environment variables to set when running the command" command-env)
(plist :key-type (restricted-sexp :match-alternatives (stringp symbolp keywordp) :tag "Variable")
:value-type (string :tag "Value")))
((const :tag "Treat stderr from adapter as program output" command-insert-stderr) boolean)
((const :tag "Working directory for command" command-cwd) (choice string symbol))
((const :tag "Path prefix for Emacs file access" prefix-local) string)
((const :tag "Path prefix for debugger file access" prefix-remote) string)
((const :tag "Host of debug adapter" host) string)
((const :tag "Port of debug adapter" port) natnum)
((const :tag "Compile cmd" compile) string)
((const :tag "Use configurationDone as trigger for launch/attach" defer-launch-attach) boolean)
((const :tag "Adapter type" :type) string)
((const :tag "Request type launch/attach" :request) string)))))
(defcustom dape-default-config-functions
'(dape-config-autoport dape-config-tramp)
"Functions applied on config before starting debugging session.
Each function is called with one argument CONFIG and should return an
PLIST of the format specified in `dape-configs'.
Functions are evaluated after functions defined in fn symbol in `dape-configs'.
See fn in `dape-configs' function signature."
:type '(repeat function))
(defcustom dape-command nil
"Initial contents for `dape' completion.
Sometimes it is useful for files or directories to supply local values
for this variable.
Example value:
\(codelldb-cc :program \"/home/user/project/a.out\")"
:type 'sexp)
;;;###autoload(put 'dape-command 'safe-local-variable #'listp)
(defcustom dape-mime-mode-alist '(("text/x-lldb.disassembly" . asm-mode)
("text/javascript" . js-mode))
"Alist of MIME types vs corresponding major mode functions.
Each element should look like (MIME-TYPE . MODE) where
MIME-TYPE is a string and MODE is the major mode function to
use for buffers of this MIME type."
:type '(alist :key-type string :value-type function))
(defcustom dape-key-prefix "\C-x\C-a"
"Prefix of all dape commands."
:type 'key-sequence)
(defcustom dape-display-source-buffer-action
`((display-buffer-use-some-window display-buffer-pop-up-window)
(some-window
. (lambda (&rest _)
(cl-loop for w in (window-list nil 'skip-minibuffer) unless
(buffer-match-p '(or (derived-mode . dape-shell-mode)
(derived-mode . dape-repl-mode)
(derived-mode . dape-memory-mode)
(derived-mode . dape-info-parent-mode))
(window-buffer w))
return w))))
"`display-buffer' action used when displaying source buffer."
:type 'sexp)
(defcustom dape-buffer-window-arrangement 'left
"Rules for display dape buffers."
:type '(choice (const :tag "GUD gdb like" gud)
(const :tag "Left side" left)
(const :tag "Right side" right)))
(defcustom dape-info-buffer-window-groups
'((dape-info-scope-mode dape-info-watch-mode)
(dape-info-stack-mode dape-info-modules-mode dape-info-sources-mode)
(dape-info-breakpoints-mode dape-info-threads-mode))
"Window display rules for `dape-info-parent-mode' derived modes.
Each list of modes is displayed in the same window. The first item of
each group is displayed by `dape-info'. All modes doesn't need to be
present in an group."
:type '(repeat (repeat function)))
(defcustom dape-variable-auto-expand-alist '((hover . 1) (repl . 1) (watch . 1))
"Default expansion depth for displaying variables.
Each entry consists of a context (such as `hover', `repl', or
`watch') paired with a number indicating how many levels deep the
variable should be expanded by default."
:type '(alist :key-type
(choice (natnum :tag "Scope number (Locals 0 etc.)")
(const :tag "Eldoc hover" hover)
(const :tag "In repl buffer" repl)
(const :tag "In watch buffer" watch)
(const :tag "All contexts" nil))
:value-type (natnum :tag "Levels expanded")))
(defcustom dape-stepping-granularity 'line
"The granularity of one step in the stepping requests."
:type '(choice (const :tag "Step statement" statement)
(const :tag "Step line" line)
(const :tag "Step instruction" instruction)))
(defcustom dape-stack-trace-levels 20
"The number of stack frames fetched."
:type 'natnum)
(defcustom dape-start-hook '(dape-repl dape-info)
"Called when session starts."
:type 'hook)
(defcustom dape-stopped-hook '(dape-memory-revert dape--emacs-grab-focus)
"Called when session stopped."
:type 'hook)
(defcustom dape-update-ui-hook '(dape-info-update)
"Called when it's sensible to refresh UI."
:type 'hook)
(defcustom dape-display-source-hook '()
"Called in buffer when placing overlay arrow for stack frame."
:type 'hook)
(defcustom dape-memory-page-size 1024
"The bytes read with `dape-read-memory'."
:type 'natnum)
(defcustom dape-info-hide-mode-line
(and (memql dape-buffer-window-arrangement '(left right)) t)
"Hide mode line in dape info buffers."
:type 'boolean)
(defcustom dape-info-variable-table-aligned nil
"Align columns in variable tables."
:type 'boolean)
(defcustom dape-info-variable-table-row-config
`((name . 0) (value . 0) (type . 0))
"Configuration for table rows of variables.
An alist that controls the display of the name, type and value of
variables. The key controls which column to change whereas the
value determines the maximum number of characters to display in each
column. A value of 0 means there is no limit.
Additionally, the order the element in the alist determines the
left-to-right display order of the properties."
:type '(alist :key-type symbol :value-type integer))
(defcustom dape-info-thread-buffer-verbose-names t
"Show long thread names in threads buffer."
:type 'boolean)
(defcustom dape-info-thread-buffer-locations t
"Show file information or library names in threads buffer."
:type 'boolean)
(defcustom dape-info-thread-buffer-addresses t
"Show addresses for thread frames in threads buffer."
:type 'boolean)
(defcustom dape-info-stack-buffer-locations t
"Show file information or library names in stack buffer."
:type 'boolean)
(defcustom dape-info-stack-buffer-modules nil
"Show module information in stack buffer if adapter supports it."
:type 'boolean)
(defcustom dape-info-stack-buffer-addresses t
"Show frame addresses in stack buffer."
:type 'boolean)
(defcustom dape-info-buffer-variable-format 'line
"How variables are formatted in *dape-info* buffer."
:type '(choice (const :tag "Truncate string at new line" line)
(const :tag "No formatting" nil)))
(defcustom dape-info-header-scope-max-name 15
"Max length of scope name in `header-line-format'."
:type 'integer)
(defcustom dape-info-file-name-max 25
"Max length of file name in dape info buffers."
:type 'integer)
(defcustom dape-breakpoint-margin-string "B"
"String to display breakpoint in margin."
:type 'string)
(defcustom dape-repl-use-shorthand t
"Dape `dape-repl-commands' can be invoked with first char of command."
:type 'boolean)
(defcustom dape-repl-commands
'(("debug" . dape)
("next" . dape-next)
("continue" . dape-continue)
("pause" . dape-pause)
("step" . dape-step-in)
("out" . dape-step-out)
("up" . dape-stack-select-up)
("down" . dape-stack-select-down)
("restart" . dape-restart)
("kill" . dape-kill)
("disconnect" . dape-disconnect-quit)
("quit" . dape-quit))
"Dape commands available in REPL buffer."
:type '(alist :key-type string
:value-type function))
(defcustom dape-compile-fn #'compile
"Function to run compile with."
:type 'function)
(defcustom dape-default-breakpoints-file
(locate-user-emacs-file "dape-breakpoints")
"Default file for loading and saving breakpoints.
See `dape-breakpoint-load' and `dape-breakpoint-save'."
:type 'file)
(defcustom dape-cwd-fn #'dape--default-cwd
"Function to get current working directory.
The function should return a string representing the absolute
file path of the current working directory, usually the current
project's root. See `dape--default-cwd'."
:type 'function)
(defcustom dape-compile-hook nil
"Called after dape compilation succeeded.
The hook is run with one argument, the compilation buffer."
:type 'hook)
(defcustom dape-minibuffer-hint-ignore-properties
'( ensure fn modes command command-args command-env command-insert-stderr
defer-launch-attach :type :request)
"Properties to be hidden in `dape--minibuffer-hint'."
:type '(repeat symbol))
(defcustom dape-minibuffer-hint t
"Show `dape-configs' hints in minibuffer."
:type 'boolean)
(defcustom dape-history-evaluated t
"Keep `dape-history' configurations evaluated.
Non-nil means each configuration read in command `dape' will be
evaluated before being pushed to `dape-history'."
:type 'boolean)
(defcustom dape-ui-debounce-time 0.1
"Number of seconds to debounce `revert-buffer' for UI buffers."
:type 'float)
(defcustom dape-request-timeout jsonrpc-default-request-timeout
"Number of seconds until a request is deemed to be timed out."
:type 'natnum)
(defcustom dape-debug nil
"Print debug info in *dape-repl* *dape-connection events* buffers."
:type 'boolean)
;;; Face
(defface dape-breakpoint-face '((t :inherit font-lock-keyword-face))
"Face used to display breakpoint overlays.")
(defface dape-log-face '((t :inherit font-lock-string-face
:height 0.85 :box (:line-width -1)))
"Face used to display log breakpoints.")
(defface dape-expression-face '((t :inherit dape-breakpoint-face
:height 0.85 :box (:line-width -1)))
"Face used to display conditional breakpoints.")
(defface dape-hits-face '((t :inherit font-lock-number-face
:height 0.85 :box (:line-width -1)))
"Face used to display hits breakpoints.")
(defface dape-exception-description-face '((t :inherit (error tooltip)
:extend t))
"Face used to display exception descriptions inline.")
(defface dape-source-line-face '((t))
"Face used to display stack frame source line overlays.")
(defface dape-repl-error-face '((t :inherit compilation-mode-line-fail
:extend t))
"Face used in repl for non 0 exit codes.")
;;; Vars
(defvar dape-history nil
"History variable for `dape'.")
;; FIXME `dape--source-buffers' should be moved into connection as
;; source references are not globally scoped.
(defvar dape--source-buffers nil
"Plist of sources reference to buffer.")
(defvar dape--breakpoints nil
"List of `dape--breakpoint's.")
(defvar dape--exceptions nil
"List of available exceptions as plists.")
(defvar dape--watched nil
"List of watched expressions.")
(defvar dape--data-breakpoints nil
"List of data breakpoints.")
(defvar dape--connection nil
"Debug adapter connection.")
(defvar dape--connection-selected nil
"Selected debug adapter connection.
If valid connection, this connection will be of highest priority when
querying for connections with `dape--live-connection'.")
(define-minor-mode dape-active-mode
"On when dape debugging session is active.
Non interactive global minor mode."
:global t
:interactive nil)
;;; Utils
(defun dape--warn (format &rest args)
"Display warning/error message with FORMAT and ARGS."
(dape--repl-insert-error (format "* %s *" (apply #'format format args))))
(defun dape--message (format &rest args)
"Display message with FORMAT and ARGS."
(dape--repl-insert (format "* %s *" (apply #'format format args))))
(defmacro dape--with-request-bind (vars fn-args &rest body)
"Call FN with ARGS and execute BODY on callback with VARS bound.
VARS are bound from the args that the callback was invoked with.
FN-ARGS is be an cons pair as FN . ARGS, where FN is expected to
take an function as an argument at ARGS + 1.
BODY is guaranteed to be evaluated with the current buffer if it's
still live.
See `cl-destructuring-bind' for bind forms."
(declare (indent 2))
(let ((old-buffer (make-symbol "old-buffer")))
`(let ((,old-buffer (current-buffer)))
(,(car fn-args) ,@(cdr fn-args)
(cl-function (lambda ,vars
(with-current-buffer (if (buffer-live-p ,old-buffer)
,old-buffer
(current-buffer))
,@body)))))))
(defmacro dape--with-request (fn-args &rest body)
"Call `dape-request' like FN with ARGS and execute BODY on callback.
FN-ARGS is be an cons pair as FN . ARGS.
BODY is guaranteed to be evaluated with the current buffer if it's
still live.
See `cl-destructuring-bind' for bind forms."
(declare (indent 1))
`(dape--with-request-bind (&rest _) ,fn-args ,@body))
(defun dape--request-continue (cb &optional error)
"Shorthand to call CB with ERROR in an `dape-request' like way."
(when (functionp cb)
(funcall cb nil error)))
(defun dape--call-with-debounce (timer backoff fn)
"Call FN with a debounce of BACKOFF seconds.
This function utilizes TIMER to store state. It cancels the TIMER
and schedules FN to run after current time + BACKOFF seconds.
If BACKOFF is non-zero, FN will be evaluated within timer context."
(cond
((zerop backoff)
(cancel-timer timer)
(funcall fn))
(t
(cancel-timer timer)
(timer-set-time timer (timer-relative-time nil backoff))
(timer-set-function timer fn)
(timer-activate timer))))
(defmacro dape--with-debounce (timer backoff &rest body)
"Eval BODY forms with a debounce of BACKOFF seconds using TIMER.
Helper macro for `dape--call-with-debounce'."
(declare (indent 2))
`(dape--call-with-debounce ,timer ,backoff (lambda () ,@body)))
(defmacro dape--with-line (buffer line &rest body)
"Save point, and current buffer; execute BODY on LINE in BUFFER."
(declare (indent 2))
`(with-current-buffer ,buffer
(save-excursion
(goto-char (point-min))
(forward-line (1- ,line))
,@body)))
(defun dape--next-like-command (conn command)
"Helper for interactive step like commands.
Run step like COMMAND on CONN. If ARG is set run COMMAND ARG times."
(if (not (dape--stopped-threads conn))
(user-error "No stopped threads")
(dape--with-request-bind
(_body error)
(dape-request conn
command
`(,@(dape--thread-id-object conn)
,@(when (dape--capable-p conn :supportsSteppingGranularity)
(list :granularity
(symbol-name dape-stepping-granularity)))))
(if error
(message "Failed to \"%s\": %s" command error)
;; From specification [continued] event:
;; A debug adapter is not expected to send this event in
;; response to a request that implies that execution
;; continues, e.g. launch or continue.
(dape-handle-event conn 'continued nil)))))
(defun dape--maybe-select-thread (conn thread-id force)
"Maybe set selected THREAD-ID and CONN.
If FORCE is non nil force thread selection.
If thread is selected, select CONN as well if no previously connection
has been selected or if current selected connection does not have any
stopped threads.
See `dape--connection-selected'."
(when (and thread-id
(or force (not (dape--thread-id conn))))
(setf (dape--thread-id conn) thread-id)
(unless (and (member dape--connection-selected (dape--live-connections))
(dape--stopped-threads dape--connection-selected))
(setq dape--connection-selected conn))))
(defun dape--threads-make-update-handle (conn)
"Return an threads update update handle for CONN.
See `dape--threads-set-status'."
(setf (dape--threads-update-handle conn)
(1+ (dape--threads-update-handle conn))))
(defun dape--threads-set-status (conn thread-id all-threads status update-handle)
"Set string STATUS thread(s) for CONN.
If THREAD-ID is non nil set status for thread with :id equal to
THREAD-ID to STATUS.
If ALL-THREADS is non nil set status of all all threads to STATUS.
Ignore status update if UPDATE-HANDLE is not the last handle created
by `dape--threads-make-update-handle'."
(when (> update-handle (dape--threads-last-update-handle conn))
(setf (dape--threads-last-update-handle conn) update-handle)
(cond ((not status) nil)
(all-threads
(cl-loop for thread in (dape--threads conn)
do (plist-put thread :status status)))
(thread-id
(plist-put
(cl-find-if (lambda (thread)
(equal (plist-get thread :id) thread-id))
(dape--threads conn))
:status status)))))
(defun dape--thread-id-object (conn)
"Construct a thread id object for CONN."
(when-let ((thread-id (dape--thread-id conn)))
(list :threadId thread-id)))
(defun dape--stopped-threads (conn)
"List of stopped threads for CONN."
(when conn
(mapcan (lambda (thread)
(when (equal (plist-get thread :status) 'stopped)
(list thread)))
(dape--threads conn))))
(defun dape--current-thread (conn)
"Current thread plist for CONN."
(when conn
(cl-find-if (lambda (thread)
(eq (plist-get thread :id) (dape--thread-id conn)))
(dape--threads conn))))
(defun dape--path-1 (conn path format)
"Return translate absolute PATH in FORMAT from CONN config.
Accepted FORMAT values are local and remote.
See `dape-configs' symbols prefix-local prefix-remote."
(if-let* (;; Fallback to last connection
(config (dape--config (or conn dape--connection)))
;; If neither `prefix-local' or `prefix-remote' is set there is
;; no work to be done
((or (plist-member config 'prefix-local)
(plist-member config 'prefix-remote)))
(absolute-path
;; `command-cwd' is always set in `dape--launch-or-attach'
(let ((command-cwd (plist-get config 'command-cwd)))
(expand-file-name path
(pcase format
('local (tramp-file-local-name command-cwd))
('remote command-cwd)))))
(prefix-local (or (plist-get config 'prefix-local) ""))