-
Notifications
You must be signed in to change notification settings - Fork 182
/
dap-python.el
308 lines (266 loc) · 13.1 KB
/
dap-python.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
;;; dap-python.el --- Debug Adapter Protocol mode for Python -*- lexical-binding: t; -*-
;; Copyright (C) 2018 Ivan Yonchovski
;; Author: Ivan Yonchovski <[email protected]>
;; Keywords: languages
;; 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Adapter for ptvsd (https://github.com/Microsoft/ptvsd)
;;; Code:
(require 'cl-lib)
(require 'dap-mode)
(defcustom dap-python-default-debug-port 32000
"The debug port which will be used for ptvsd process.
If the port is taken, DAP will try the next port."
:group 'dap-python
:type 'number)
(defcustom dap-python-executable "python"
"The python executable to use."
:group 'dap-python
:risky t
:type 'file)
(defcustom dap-python-terminal nil
"The terminal to use when running the debug process.
For example you may set it to `xterm -e' which will pop xterm console when
you are debugging."
:group 'dap-python
:risky t
:type '(choice (string) (const :tag "None" nil)))
(defun dap-python--pyenv-executable-find (command)
"Find executable COMMAND, taking pyenv shims into account.
If the executable is a system executable and not in the same path
as the pyenv version then also return nil. This works around
https://github.com/pyenv/pyenv-which-ext."
(if (executable-find "pyenv")
(progn
(let ((pyenv-string (shell-command-to-string (concat "pyenv which " command)))
(pyenv-version-names (split-string (string-trim (shell-command-to-string "pyenv version-name")) ":"))
(executable nil)
(i 0))
(if (not (string-match "not found" pyenv-string))
(while (and (not executable)
(< i (length pyenv-version-names)))
(if (string-match (elt pyenv-version-names i) (string-trim pyenv-string))
(setq executable (string-trim pyenv-string)))
(if (string-match (elt pyenv-version-names i) "system")
(setq executable (string-trim (executable-find command))))
(setq i (1+ i))))
executable))
(executable-find command)))
(cl-defstruct dap-python--point
(line nil :type integer)
(character nil :type integer))
(cl-defstruct dap-python--location
(start nil :type dap-python--point)
(end nil :type dap-python--point))
(cl-defstruct dap-python--symbol
(name nil :type string)
(type nil :type string)
(location nil :type dap-python--location))
(lsp-defun dap-python--parse-lsp-symbol
((&DocumentSymbol
:name :kind
:range (&Range :start (&Position :line start-line
:character start-character)
:end (&Position :line end-line
:character end-character))))
(make-dap-python--symbol
:name name
:type (alist-get kind lsp-symbol-kinds)
:location (make-dap-python--location
:start (make-dap-python--point :line start-line
:character start-character)
:end (make-dap-python--point :line end-line
:character end-character))))
(defun dap-python--symbol-before-point (point lsp-symbol)
(-> lsp-symbol
dap-python--symbol-location
dap-python--location-start
dap-python--point-line
(< (dap-python--point-line point))))
(defun dap-python--symbols-before-point (point lsp-symbols)
(-filter (-partial 'dap-python--symbol-before-point point) lsp-symbols))
(defun dap-python--test-p (lsp-symbol)
(let ((name (dap-python--symbol-name lsp-symbol)))
(and (or (string= (dap-python--symbol-type lsp-symbol) "Function")
(string= (dap-python--symbol-type lsp-symbol) "Method"))
(s-starts-with? "test" name))))
(defun dap-python--test-class-p (test-symbol lsp-symbol)
(when (string= (dap-python--symbol-type lsp-symbol) "Class")
(let* ((class-location (dap-python--symbol-location lsp-symbol))
(class-start-line (-> class-location dap-python--location-start dap-python--point-line))
(class-end-line (-> class-location dap-python--location-end dap-python--point-line))
(test-start-line (-> test-symbol dap-python--symbol-location dap-python--location-start dap-python--point-line)))
(and (> test-start-line class-start-line)
(< test-start-line class-end-line)))))
(defun dap-python--nearest-test (lsp-symbols)
(cl-callf reverse lsp-symbols)
(when-let ((test-symbol (-first 'dap-python--test-p lsp-symbols)))
(let ((class-symbol
(-first (-partial 'dap-python--test-class-p test-symbol)
lsp-symbols)))
(if class-symbol
(concat "::" (dap-python--symbol-name class-symbol)
"::" (dap-python--symbol-name test-symbol))
(concat "::" (dap-python--symbol-name test-symbol))))))
(defun dap-python--cursor-position ()
(make-dap-python--point :line (line-number-at-pos)
:character (current-column)))
(defun dap-python--test-at-point ()
(->> (lsp--get-document-symbols)
(lsp--symbols->document-symbols-hierarchy)
(mapcar #'dap-python--parse-lsp-symbol)
(dap-python--symbols-before-point (dap-python--cursor-position))
dap-python--nearest-test))
(defun dap-python--template (template-name)
"Return the debug template whose name is TEMPLATE-NAME.
For the name, only the template's `car' is checked, not its
`:name' property."
(cdr (--first (string= template-name (car it)) dap-debug-template-configurations)))
(defalias 'dap-python--debug-test-at-point #'dap-python-debug-test-at-point)
(defun dap-python-debug-test-at-point ()
"Debug the pytest test under the cursor."
(interactive)
(dap-debug (dap-python--template "Python :: Run pytest (at point)")))
(defcustom dap-python-debugger 'ptvsd
"Specify which debugger to use for `dap-python'.
Can be either `ptvsd' or `debugpy.' Note that this setting can be
overridden in individual `dap-python' launch configuration. The
values of this variable or the :debugger field may also be
strings, for the sake of launch.json feature parity."
:type '(choice (const ptvsd) (const debugpy))
:group 'dap-python)
(defun dap-python--populate-start-file-args (conf)
"Populate CONF with the required arguments."
(let* ((python-executable (dap-python--pyenv-executable-find dap-python-executable))
(python-args (plist-get conf :args))
(program (or (plist-get conf :target-module)
(plist-get conf :program)
(buffer-file-name)))
(module (plist-get conf :module))
(debugger (plist-get conf :debugger))
(targetPid (plist-get conf :processId)))
;; These are `dap-python'-specific and always ignored.
(cl-remf conf :debugger)
(cl-remf conf :target-module)
;; Ignored by ptsvd and set explicitly for debugpy.
(cl-remf conf :program)
(pcase (or debugger dap-python-debugger)
((or 'ptvsd "ptvsd")
(let ((host "localhost")
(debug-port (dap--find-available-port)))
;; support :args ["foo" "bar"]; NOTE: :args can be nil; however, nil is
;; a list, so it will be mapconcat'ed, yielding the empty string.
(when (sequencep python-args)
(setq python-args (mapconcat #'shell-quote-argument python-args " ")))
;; ignored by ptsvd anyway
(cl-remf conf :module)
(cl-remf conf :args)
(plist-put conf :program-to-start
(format "%s%s -m ptvsd --wait --host %s --port %s %s"
(or dap-python-terminal "")
(shell-quote-argument python-executable)
host
debug-port
(if targetPid
(format "--pid %s" targetPid)
(format "%s %s %s"
(if module (concat " -m " (shell-quote-argument module)) "")
(if program (shell-quote-argument program) "")
(if (not (string-empty-p python-args)) (concat " " python-args) "")))))
(plist-put conf :debugServer debug-port)
(plist-put conf :port debug-port)
(plist-put conf :hostName host)
(plist-put conf :host host)))
((or 'debugpy "debugpy")
(cond ((stringp python-args)
(cl-callf split-string-and-unquote python-args))
;; If both :module and :program are specified, we'll need to push
;; :program to PYTHON-ARGS instead, to behave like ptvsd. This is
;; needed for the debug-test-at-point functionality.
((and (vectorp python-args) module program)
(cl-callf cl-coerce python-args 'list)))
;; If certain properties are nil, issues will arise, as debugpy expects
;; them to unspecified instead. Some templates in this file set such
;; properties (e.g. :module) to nil instead of leaving them undefined. To
;; support them, sanitize CONF before passing it on.
(when program
(if module
(push program python-args)
(plist-put conf :program program)))
(cl-remf conf :args)
(plist-put conf :args (or python-args []))
(unless module
(cl-remf conf :module))
(unless (plist-get conf :cwd)
(cl-remf conf :cwd))
(pcase (cons (plist-get conf :request) targetPid)
((or `("launch" . nil)
`("attach" . ,(and pid (guard pid))))
(plist-put conf :dap-server-path
(list python-executable "-m" "debugpy.adapter")))
(`("attach" . nil)
(let* ((connect (plist-get conf :connect))
(host (or (plist-get connect :host) "localhost"))
(port (or (plist-get connect :port) 5678)))
(plist-put conf :host host)
(plist-put conf :debugServer port)
(cl-remf conf :connect)))))
(_ (error "`dap-python': unknown :debugger type %S" debugger)))
conf))
(defun dap-python--populate-test-at-point (conf)
"Populate CONF with the required arguments."
(if-let ((test (dap-python--test-at-point)))
(plist-put conf :program (concat (buffer-file-name) test))
(user-error "`dap-python': no test at point"))
(plist-put conf :cwd (lsp-workspace-root))
(dap-python--populate-start-file-args conf))
(dap-register-debug-provider "python" 'dap-python--populate-start-file-args)
(dap-register-debug-template "Python :: Attach to running process"
(list :type "python"
:request "attach"
:processId "${command:pickProcess}"
:name "Python :: Attach to running process"))
(dap-register-debug-template "Python :: Run file (buffer)"
(list :type "python"
:args ""
:cwd nil
:module nil
:program nil
:request "launch"
:name "Python :: Run file (buffer)"))
(dap-register-debug-template "Python :: Run file from project directory"
(list :type "python"
:args ""
:cwd "${workspaceFolder}"
:module nil
:program nil
:request "launch"))
(dap-register-debug-template "Python :: Run pytest (buffer)"
(list :type "python"
:args ""
:cwd nil
:program nil
:module "pytest"
:request "launch"
:name "Python :: Run pytest (buffer)"))
(dap-register-debug-provider "python-test-at-point" 'dap-python--populate-test-at-point)
(dap-register-debug-template "Python :: Run pytest (at point)"
(list :type "python"
:args ""
:program nil
:module "pytest"
:request "launch"
:name "Python :: Run pytest (at point)"))
(cl-defmethod dap-handle-event ((_event-type (eql debugpyWaitingForServer)) _session _params))
(cl-defmethod dap-handle-event ((_event-type (eql debugpyAttach)) _session _params))
(provide 'dap-python)
;;; dap-python.el ends here