emacs-lsp / dap-mode

Emacs :heart: Debug Adapter Protocol
https://emacs-lsp.github.io/dap-mode
GNU General Public License v3.0
1.3k stars 182 forks source link

[feature request] add debug-to-here and go-back-to-current-debug-step fucntion #379

Open wztdream opened 4 years ago

wztdream commented 4 years ago

Hi, Thank you all for this great package! It will be handy if dap-mode can add debug-to-here function, I know we can add and remove break point for this purpose, but it will be convenient for the users to make it auto, move cursor to some line, then debug-to-here, that is great.

Sometimes we need to move our cursor around during debug, to check some code for example, I always get lost and can not find where is current debug step, it will be useful to have a function go-back-to-current-debug-step to jump back to current debug step.

wztdream commented 4 years ago

@yyoncho I try to implement dap-run-to-cursor by wrapping dap-breakpoint-add dap-continue and dap-breakpoint-delete

(defun dap-run-to-cursor ()
  "Run to cursor"
  (interactive)
  (save-excursion
    (call-interactively #'dap-breakpoint-add)
    (call-interactively #'dap-continue)
    )
  (call-interactively #'dap-breakpoint-delete)
  )

But results in error:

Debugger entered--Lisp error: (wrong-type-argument overlayp nil)
  delete-overlay(nil)
  mapc(delete-overlay (nil #<overlay from 1 to 9 in dap-test.py> #<overlay from 10 to 18 in dap-test.py>))
  dap-ui--clear-breakpoint-overlays()
  dap-ui--refresh-breakpoints()
  run-hooks(dap-breakpoints-changed-hook)
  dap--update-breakpoints(#s(dap--debug-session :name "Python :: Run file (buffer)" :last-id 34 :proc #<process Python :: Run file (buffer)> :response-handlers #<hash-table eql 2/65 0x158b1615ecc5> :parser #s(dap--parser :waiting-for-response nil :response-result nil :headers nil :body nil :reading-body nil :body-length nil :body-received nil :leftovers "") :output-buffer #<buffer *Python :: Run file (buffer) out*> :thread-id 1 :workspace nil :threads (#<hash-table equal 2/65 0x158b168d7ced>) :thread-states #<hash-table eql 1/65 0x158b161683bd> :active-frame-id nil :active-frame nil :cursor-marker nil :state running :breakpoints #<hash-table equal 1/65 0x158b16168695> :thread-stack-frames #<hash-table eql 0/65 0x158b161686b5> :launch-args (:type "python" :request "launch" :name "Python :: Run file (buffer)" :args [] :dap-server-path ("/home/wangzongtao/anaconda3/envs/py36/bin/python" "-m" "debugpy.adapter") :program "/home/wangzongtao/research/code/temp/dap-test.py") :initialize-result #<hash-table equal 6/65 0x158b1613a055> :error-message nil :loaded-sources nil :program-proc nil :metadata #<hash-table eql 0/65 0x158b161686d5> :output-displayed nil) #<hash-table equal 6/65 0x158b170d8209> "/home/wangzongtao/research/code/temp/dap-test.py")
  #f(compiled-function (resp) #<bytecode 0x158b16fd537d>)(#<hash-table equal 6/65 0x158b170d8209>)
  #f(compiled-function (input0) #<bytecode 0x158b16fd5399>)(#<hash-table equal 6/65 0x158b170d8209>)
  #f(compiled-function (m) #<bytecode 0x158b17028af1>)("{\"seq\": 51, \"type\": \"response\", \"request_seq\": 33,...")
  mapc(#f(compiled-function (m) #<bytecode 0x158b17028af1>) ("{\"seq\": 45, \"type\": \"response\", \"request_seq\": 31,..." "{\"seq\": 46, \"type\": \"response\", \"request_seq\": 32,..." "{\"seq\": 47, \"type\": \"event\", \"event\": \"output\", \"b..." "{\"seq\": 48, \"type\": \"event\", \"event\": \"continued\",..." "{\"seq\": 49, \"type\": \"event\", \"event\": \"stopped\", \"..." "{\"seq\": 50, \"type\": \"event\", \"event\": \"output\", \"b..." "{\"seq\": 51, \"type\": \"response\", \"request_seq\": 33,..."))
  #f(compiled-function (_ msg) #<bytecode 0x158b1615f529>)(#<process Python :: Run file (buffer)> "Content-Length: 528\15\n\15\n{\"seq\": 45, \"type\": \"respon...")

So, where am I wrong?

yyoncho commented 4 years ago

(defun dap-run-to-cursor () "Run to cursor" (interactive) (save-excursion (call-interactively #'dap-breakpoint-add) (call-interactively #'dap-continue) ) (call-interactively #'dap-breakpoint-delete) )

This wont work since dap-breakpoint-add/continue are async operations. Generally, I don't think that this functionality can be implemented without server support.

wztdream commented 4 years ago

So DAP server does not support this function?

But I do see the run-to-cursor function in vscode, and I noticed that it temporally added and deleted a break-point (a break point appear and disappear quickly in break point window), so it seems vscode achieve this by just add and deleted the break point.

yyoncho commented 4 years ago

It most likely uses https://microsoft.github.io/debug-adapter-protocol/specification#Requests_GotoTargets

yyoncho commented 4 years ago

... and https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Goto

wztdream commented 4 years ago

Thank you, but it seems this beyond my ability. I have only limited elisp knowledge and I do not understand how dap-mode handle the request and responds. Maybe someone are interested to implement it, it should be useful.

nbfalcon commented 4 years ago

Goto has nothing to do with this afaics - it allows moving the instruction pointer to some line of code, skipping things. This isn't what we need. I do believe that this can be implemented without debugger support - we need to add callbacks to breakpoints, and the ability to make breakpoints invisible. This way, an invisible, self-deleting breakpoint could be added.

nbfalcon commented 4 years ago

But there will be lots of refactoring to do - breakpoints are plists, not structures sadly.

nbfalcon commented 4 years ago

Actually, this could be even simpler; dap-breakpoint-add with dap-breakpoint-delete in the async body.

nbfalcon commented 4 years ago

I just tried and failed: we need callbacks. Here is my (failed) attempt for anyone interested:

(defun dap-run-until (session pos file)
  "Continue until the cursor is hit."
  (interactive (list (dap--cur-session-or-die) (point) (buffer-file-name)))
  (let ((file-breakpoints (gethash file (dap--get-breakpoints)))
        (new-bp (list :point pos)))
    (dap--send-message
     (dap--set-breakpoints-request file (cons new-bp file-breakpoints))
     (dap--resp-handler
      (lambda (_) (dap--send-message
              (dap--set-breakpoints-request file file-breakpoints)
              (dap--resp-handler) session))) session)))
yyoncho commented 4 years ago

But there will be lots of refactoring to do - breakpoints are plists, not structures sadly.

That should be fine.

I checked vscode and it is using breakpoints but their solution does not work the way run to cursor works in the IDEs.

  1. The "invisible" breakpoint can be hit by other threads
  2. The breakpoints before 'run to here' are hit as well.
nbfalcon commented 4 years ago

@yyoncho 2: we could temporarily disable all breakpoints, but not display that in the GUI. 1: this will be a problem.

nbfalcon commented 4 years ago

Does dap have thread-local breakpoints?

yyoncho commented 4 years ago

We cannot disable temporary the breakpoints because this will affect other threads. There are no thread local breakpoints. We could have ad-hoc solution like VScode

nbfalcon commented 4 years ago

But what are are the limitations of VSCode's solution? Does it work in multithreaded programs, does the program break on other breakpoints? If not, then I have one more final idea: we can have a temporary breakpoints whitelist, which maps threads to the breakpoints that are allowed to break them, coupled with adding an :invisible property to breakpoints. If we hit any other breakpoints, we just issue a continue request and ignore them one dap's side.

yyoncho commented 4 years ago

AFAICS their solution is simple:

  1. Create an invisible breakpoint with callback to delete itself when hit.
  2. Call continue on the current thread on the callback of setBreakpoints
nbfalcon commented 4 years ago

But that means that the current thread would be stopped when hitting other breakpoints, and that other threads could be stopped too, or am I missing something?

yyoncho commented 4 years ago

But that means that the current thread would be stopped when hitting other breakpoints, and that other threads could be stopped too, or am I missing something?

Yes. The question is whether their solution is good enough from a practical point of view or we should spend more effort on implementing what you have described above.