1" Debugger plugin using gdb.
2"
3" Author: Bram Moolenaar
4" Copyright: Vim license applies, see ":help license"
5" Last Change: 2021 Aug 23
6"
7" WORK IN PROGRESS - Only the basics work
8" Note: On MS-Windows you need a recent version of gdb.  The one included with
9" MingW is too old (7.6.1).
10" I used version 7.12 from http://www.equation.com/servlet/equation.cmd?fa=gdb
11"
12" There are two ways to run gdb:
13" - In a terminal window; used if possible, does not work on MS-Windows
14"   Not used when g:termdebug_use_prompt is set to 1.
15" - Using a "prompt" buffer; may use a terminal window for the program
16"
17" For both the current window is used to view source code and shows the
18" current statement from gdb.
19"
20" USING A TERMINAL WINDOW
21"
22" Opens two visible terminal windows:
23" 1. runs a pty for the debugged program, as with ":term NONE"
24" 2. runs gdb, passing the pty of the debugged program
25" A third terminal window is hidden, it is used for communication with gdb.
26"
27" USING A PROMPT BUFFER
28"
29" Opens a window with a prompt buffer to communicate with gdb.
30" Gdb is run as a job with callbacks for I/O.
31" On Unix another terminal window is opened to run the debugged program
32" On MS-Windows a separate console is opened to run the debugged program
33"
34" The communication with gdb uses GDB/MI.  See:
35" https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html
36
37" In case this gets sourced twice.
38if exists(':Termdebug')
39  finish
40endif
41
42" Need either the +terminal feature or +channel and the prompt buffer.
43" The terminal feature does not work with gdb on win32.
44if has('terminal') && !has('win32')
45  let s:way = 'terminal'
46elseif has('channel') && exists('*prompt_setprompt')
47  let s:way = 'prompt'
48else
49  if has('terminal')
50    let s:err = 'Cannot debug, missing prompt buffer support'
51  else
52    let s:err = 'Cannot debug, +channel feature is not supported'
53  endif
54  command -nargs=* -complete=file -bang Termdebug echoerr s:err
55  command -nargs=+ -complete=file -bang TermdebugCommand echoerr s:err
56  finish
57endif
58
59let s:keepcpo = &cpo
60set cpo&vim
61
62" The command that starts debugging, e.g. ":Termdebug vim".
63" To end type "quit" in the gdb window.
64command -nargs=* -complete=file -bang Termdebug call s:StartDebug(<bang>0, <f-args>)
65command -nargs=+ -complete=file -bang TermdebugCommand call s:StartDebugCommand(<bang>0, <f-args>)
66
67" Name of the gdb command, defaults to "gdb".
68if !exists('g:termdebugger')
69  let g:termdebugger = 'gdb'
70endif
71
72let s:pc_id = 12
73let s:asm_id = 13
74let s:break_id = 14  " breakpoint number is added to this
75let s:stopped = 1
76
77let s:parsing_disasm_msg = 0
78let s:asm_lines = []
79let s:asm_addr = ''
80
81" Take a breakpoint number as used by GDB and turn it into an integer.
82" The breakpoint may contain a dot: 123.4 -> 123004
83" The main breakpoint has a zero subid.
84func s:Breakpoint2SignNumber(id, subid)
85  return s:break_id + a:id * 1000 + a:subid
86endfunction
87
88func s:Highlight(init, old, new)
89  let default = a:init ? 'default ' : ''
90  if a:new ==# 'light' && a:old !=# 'light'
91    exe "hi " . default . "debugPC term=reverse ctermbg=lightblue guibg=lightblue"
92  elseif a:new ==# 'dark' && a:old !=# 'dark'
93    exe "hi " . default . "debugPC term=reverse ctermbg=darkblue guibg=darkblue"
94  endif
95endfunc
96
97call s:Highlight(1, '', &background)
98hi default debugBreakpoint term=reverse ctermbg=red guibg=red
99
100func s:StartDebug(bang, ...)
101  " First argument is the command to debug, second core file or process ID.
102  call s:StartDebug_internal({'gdb_args': a:000, 'bang': a:bang})
103endfunc
104
105func s:StartDebugCommand(bang, ...)
106  " First argument is the command to debug, rest are run arguments.
107  call s:StartDebug_internal({'gdb_args': [a:1], 'proc_args': a:000[1:], 'bang': a:bang})
108endfunc
109
110func s:StartDebug_internal(dict)
111  if exists('s:gdbwin')
112    echoerr 'Terminal debugger already running, cannot run two'
113    return
114  endif
115  if !executable(g:termdebugger)
116    echoerr 'Cannot execute debugger program "' .. g:termdebugger .. '"'
117    return
118  endif
119
120  let s:ptywin = 0
121  let s:pid = 0
122  let s:asmwin = 0
123
124  if exists('#User#TermdebugStartPre')
125    doauto <nomodeline> User TermdebugStartPre
126  endif
127
128  " Uncomment this line to write logging in "debuglog".
129  " call ch_logfile('debuglog', 'w')
130
131  let s:sourcewin = win_getid(winnr())
132
133  " Remember the old value of 'signcolumn' for each buffer that it's set in, so
134  " that we can restore the value for all buffers.
135  let b:save_signcolumn = &signcolumn
136  let s:signcolumn_buflist = [bufnr()]
137
138  let s:save_columns = 0
139  let s:allleft = 0
140  if exists('g:termdebug_wide')
141    if &columns < g:termdebug_wide
142      let s:save_columns = &columns
143      let &columns = g:termdebug_wide
144      " If we make the Vim window wider, use the whole left halve for the debug
145      " windows.
146      let s:allleft = 1
147    endif
148    let s:vertical = 1
149  else
150    let s:vertical = 0
151  endif
152
153  " Override using a terminal window by setting g:termdebug_use_prompt to 1.
154  let use_prompt = exists('g:termdebug_use_prompt') && g:termdebug_use_prompt
155  if has('terminal') && !has('win32') && !use_prompt
156    let s:way = 'terminal'
157  else
158    let s:way = 'prompt'
159  endif
160
161  if s:way == 'prompt'
162    call s:StartDebug_prompt(a:dict)
163  else
164    call s:StartDebug_term(a:dict)
165  endif
166
167  if exists('g:termdebug_disasm_window')
168    if g:termdebug_disasm_window
169      let curwinid = win_getid(winnr())
170      call s:GotoAsmwinOrCreateIt()
171      call win_gotoid(curwinid)
172    endif
173  endif
174
175  if exists('#User#TermdebugStartPost')
176    doauto <nomodeline> User TermdebugStartPost
177  endif
178endfunc
179
180" Use when debugger didn't start or ended.
181func s:CloseBuffers()
182  exe 'bwipe! ' . s:ptybuf
183  exe 'bwipe! ' . s:commbuf
184  unlet! s:gdbwin
185endfunc
186
187func s:CheckGdbRunning()
188  let gdbproc = term_getjob(s:gdbbuf)
189  if gdbproc == v:null || job_status(gdbproc) !=# 'run'
190    echoerr string(g:termdebugger) . ' exited unexpectedly'
191    call s:CloseBuffers()
192    return ''
193  endif
194  return 'ok'
195endfunc
196
197func s:StartDebug_term(dict)
198  " Open a terminal window without a job, to run the debugged program in.
199  let s:ptybuf = term_start('NONE', {
200	\ 'term_name': 'debugged program',
201	\ 'vertical': s:vertical,
202	\ })
203  if s:ptybuf == 0
204    echoerr 'Failed to open the program terminal window'
205    return
206  endif
207  let pty = job_info(term_getjob(s:ptybuf))['tty_out']
208  let s:ptywin = win_getid(winnr())
209  if s:vertical
210    " Assuming the source code window will get a signcolumn, use two more
211    " columns for that, thus one less for the terminal window.
212    exe (&columns / 2 - 1) . "wincmd |"
213    if s:allleft
214      " use the whole left column
215      wincmd H
216    endif
217  endif
218
219  " Create a hidden terminal window to communicate with gdb
220  let s:commbuf = term_start('NONE', {
221	\ 'term_name': 'gdb communication',
222	\ 'out_cb': function('s:CommOutput'),
223	\ 'hidden': 1,
224	\ })
225  if s:commbuf == 0
226    echoerr 'Failed to open the communication terminal window'
227    exe 'bwipe! ' . s:ptybuf
228    return
229  endif
230  let commpty = job_info(term_getjob(s:commbuf))['tty_out']
231
232  " Open a terminal window to run the debugger.
233  " Add -quiet to avoid the intro message causing a hit-enter prompt.
234  let gdb_args = get(a:dict, 'gdb_args', [])
235  let proc_args = get(a:dict, 'proc_args', [])
236
237  let cmd = [g:termdebugger, '-quiet', '-tty', pty, '--eval-command', 'echo startupdone\n'] + gdb_args
238  call ch_log('executing "' . join(cmd) . '"')
239  let s:gdbbuf = term_start(cmd, {
240	\ 'term_finish': 'close',
241	\ })
242  if s:gdbbuf == 0
243    echoerr 'Failed to open the gdb terminal window'
244    call s:CloseBuffers()
245    return
246  endif
247  let s:gdbwin = win_getid(winnr())
248
249  " Wait for the "startupdone" message before sending any commands.
250  let try_count = 0
251  while 1
252    if s:CheckGdbRunning() != 'ok'
253      return
254    endif
255
256    for lnum in range(1, 200)
257      if term_getline(s:gdbbuf, lnum) =~ 'startupdone'
258	let try_count = 9999
259	break
260      endif
261    endfor
262    let try_count += 1
263    if try_count > 300
264      " done or give up after five seconds
265      break
266    endif
267    sleep 10m
268  endwhile
269
270  " Set arguments to be run.
271  if len(proc_args)
272    call term_sendkeys(s:gdbbuf, 'set args ' . join(proc_args) . "\r")
273  endif
274
275  " Connect gdb to the communication pty, using the GDB/MI interface
276  call term_sendkeys(s:gdbbuf, 'new-ui mi ' . commpty . "\r")
277
278  " Wait for the response to show up, users may not notice the error and wonder
279  " why the debugger doesn't work.
280  let try_count = 0
281  while 1
282    if s:CheckGdbRunning() != 'ok'
283      return
284    endif
285
286    let response = ''
287    for lnum in range(1, 200)
288      let line1 = term_getline(s:gdbbuf, lnum)
289      let line2 = term_getline(s:gdbbuf, lnum + 1)
290      if line1 =~ 'new-ui mi '
291	" response can be in the same line or the next line
292	let response = line1 . line2
293	if response =~ 'Undefined command'
294	  echoerr 'Sorry, your gdb is too old, gdb 7.12 is required'
295	  call s:CloseBuffers()
296	  return
297	endif
298	if response =~ 'New UI allocated'
299	  " Success!
300	  break
301	endif
302      elseif line1 =~ 'Reading symbols from' && line2 !~ 'new-ui mi '
303	" Reading symbols might take a while, try more times
304	let try_count -= 1
305      endif
306    endfor
307    if response =~ 'New UI allocated'
308      break
309    endif
310    let try_count += 1
311    if try_count > 100
312      echoerr 'Cannot check if your gdb works, continuing anyway'
313      break
314    endif
315    sleep 10m
316  endwhile
317
318  " Interpret commands while the target is running.  This should usually only be
319  " exec-interrupt, since many commands don't work properly while the target is
320  " running.
321  call s:SendCommand('-gdb-set mi-async on')
322  " Older gdb uses a different command.
323  call s:SendCommand('-gdb-set target-async on')
324
325  " Disable pagination, it causes everything to stop at the gdb
326  " "Type <return> to continue" prompt.
327  call s:SendCommand('set pagination off')
328
329  call job_setoptions(term_getjob(s:gdbbuf), {'exit_cb': function('s:EndTermDebug')})
330
331  " Set the filetype, this can be used to add mappings.
332  set filetype=termdebug
333
334  call s:StartDebugCommon(a:dict)
335endfunc
336
337func s:StartDebug_prompt(dict)
338  " Open a window with a prompt buffer to run gdb in.
339  if s:vertical
340    vertical new
341  else
342    new
343  endif
344  let s:gdbwin = win_getid(winnr())
345  let s:promptbuf = bufnr('')
346  call prompt_setprompt(s:promptbuf, 'gdb> ')
347  set buftype=prompt
348  file gdb
349  call prompt_setcallback(s:promptbuf, function('s:PromptCallback'))
350  call prompt_setinterrupt(s:promptbuf, function('s:PromptInterrupt'))
351
352  if s:vertical
353    " Assuming the source code window will get a signcolumn, use two more
354    " columns for that, thus one less for the terminal window.
355    exe (&columns / 2 - 1) . "wincmd |"
356  endif
357
358  " Add -quiet to avoid the intro message causing a hit-enter prompt.
359  let gdb_args = get(a:dict, 'gdb_args', [])
360  let proc_args = get(a:dict, 'proc_args', [])
361
362  let cmd = [g:termdebugger, '-quiet', '--interpreter=mi2'] + gdb_args
363  call ch_log('executing "' . join(cmd) . '"')
364
365  let s:gdbjob = job_start(cmd, {
366	\ 'exit_cb': function('s:EndPromptDebug'),
367	\ 'out_cb': function('s:GdbOutCallback'),
368	\ })
369  if job_status(s:gdbjob) != "run"
370    echoerr 'Failed to start gdb'
371    exe 'bwipe! ' . s:promptbuf
372    return
373  endif
374  " Mark the buffer modified so that it's not easy to close.
375  set modified
376  let s:gdb_channel = job_getchannel(s:gdbjob)
377
378  " Interpret commands while the target is running.  This should usually only
379  " be exec-interrupt, since many commands don't work properly while the
380  " target is running.
381  call s:SendCommand('-gdb-set mi-async on')
382  " Older gdb uses a different command.
383  call s:SendCommand('-gdb-set target-async on')
384
385  let s:ptybuf = 0
386  if has('win32')
387    " MS-Windows: run in a new console window for maximum compatibility
388    call s:SendCommand('set new-console on')
389  elseif has('terminal')
390    " Unix: Run the debugged program in a terminal window.  Open it below the
391    " gdb window.
392    belowright let s:ptybuf = term_start('NONE', {
393	  \ 'term_name': 'debugged program',
394	  \ })
395    if s:ptybuf == 0
396      echoerr 'Failed to open the program terminal window'
397      call job_stop(s:gdbjob)
398      return
399    endif
400    let s:ptywin = win_getid(winnr())
401    let pty = job_info(term_getjob(s:ptybuf))['tty_out']
402    call s:SendCommand('tty ' . pty)
403
404    " Since GDB runs in a prompt window, the environment has not been set to
405    " match a terminal window, need to do that now.
406    call s:SendCommand('set env TERM = xterm-color')
407    call s:SendCommand('set env ROWS = ' . winheight(s:ptywin))
408    call s:SendCommand('set env LINES = ' . winheight(s:ptywin))
409    call s:SendCommand('set env COLUMNS = ' . winwidth(s:ptywin))
410    call s:SendCommand('set env COLORS = ' . &t_Co)
411    call s:SendCommand('set env VIM_TERMINAL = ' . v:version)
412  else
413    " TODO: open a new terminal get get the tty name, pass on to gdb
414    call s:SendCommand('show inferior-tty')
415  endif
416  call s:SendCommand('set print pretty on')
417  call s:SendCommand('set breakpoint pending on')
418  " Disable pagination, it causes everything to stop at the gdb
419  call s:SendCommand('set pagination off')
420
421  " Set arguments to be run
422  if len(proc_args)
423    call s:SendCommand('set args ' . join(proc_args))
424  endif
425
426  call s:StartDebugCommon(a:dict)
427  startinsert
428endfunc
429
430func s:StartDebugCommon(dict)
431  " Sign used to highlight the line where the program has stopped.
432  " There can be only one.
433  sign define debugPC linehl=debugPC
434
435  " Install debugger commands in the text window.
436  call win_gotoid(s:sourcewin)
437  call s:InstallCommands()
438  call win_gotoid(s:gdbwin)
439
440  " Enable showing a balloon with eval info
441  if has("balloon_eval") || has("balloon_eval_term")
442    set balloonexpr=TermDebugBalloonExpr()
443    if has("balloon_eval")
444      set ballooneval
445    endif
446    if has("balloon_eval_term")
447      set balloonevalterm
448    endif
449  endif
450
451  " Contains breakpoints that have been placed, key is a string with the GDB
452  " breakpoint number.
453  " Each entry is a dict, containing the sub-breakpoints.  Key is the subid.
454  " For a breakpoint that is just a number the subid is zero.
455  " For a breakpoint "123.4" the id is "123" and subid is "4".
456  " Example, when breakpoint "44", "123", "123.1" and "123.2" exist:
457  " {'44': {'0': entry}, '123': {'0': entry, '1': entry, '2': entry}}
458  let s:breakpoints = {}
459
460  " Contains breakpoints by file/lnum.  The key is "fname:lnum".
461  " Each entry is a list of breakpoint IDs at that position.
462  let s:breakpoint_locations = {}
463
464  augroup TermDebug
465    au BufRead * call s:BufRead()
466    au BufUnload * call s:BufUnloaded()
467    au OptionSet background call s:Highlight(0, v:option_old, v:option_new)
468  augroup END
469
470  " Run the command if the bang attribute was given and got to the debug
471  " window.
472  if get(a:dict, 'bang', 0)
473    call s:SendCommand('-exec-run')
474    call win_gotoid(s:ptywin)
475  endif
476endfunc
477
478" Send a command to gdb.  "cmd" is the string without line terminator.
479func s:SendCommand(cmd)
480  call ch_log('sending to gdb: ' . a:cmd)
481  if s:way == 'prompt'
482    call ch_sendraw(s:gdb_channel, a:cmd . "\n")
483  else
484    call term_sendkeys(s:commbuf, a:cmd . "\r")
485  endif
486endfunc
487
488" This is global so that a user can create their mappings with this.
489func TermDebugSendCommand(cmd)
490  if s:way == 'prompt'
491    call ch_sendraw(s:gdb_channel, a:cmd . "\n")
492  else
493    let do_continue = 0
494    if !s:stopped
495      let do_continue = 1
496      call s:SendCommand('-exec-interrupt')
497      sleep 10m
498    endif
499    call term_sendkeys(s:gdbbuf, a:cmd . "\r")
500    if do_continue
501      Continue
502    endif
503  endif
504endfunc
505
506" Function called when entering a line in the prompt buffer.
507func s:PromptCallback(text)
508  call s:SendCommand(a:text)
509endfunc
510
511" Function called when pressing CTRL-C in the prompt buffer and when placing a
512" breakpoint.
513func s:PromptInterrupt()
514  call ch_log('Interrupting gdb')
515  if has('win32')
516    " Using job_stop() does not work on MS-Windows, need to send SIGTRAP to
517    " the debugger program so that gdb responds again.
518    if s:pid == 0
519      echoerr 'Cannot interrupt gdb, did not find a process ID'
520    else
521      call debugbreak(s:pid)
522    endif
523  else
524    call job_stop(s:gdbjob, 'int')
525  endif
526endfunc
527
528" Function called when gdb outputs text.
529func s:GdbOutCallback(channel, text)
530  call ch_log('received from gdb: ' . a:text)
531
532  " Drop the gdb prompt, we have our own.
533  " Drop status and echo'd commands.
534  if a:text == '(gdb) ' || a:text == '^done' || a:text[0] == '&'
535    return
536  endif
537  if a:text =~ '^^error,msg='
538    let text = s:DecodeMessage(a:text[11:])
539    if exists('s:evalexpr') && text =~ 'A syntax error in expression, near\|No symbol .* in current context'
540      " Silently drop evaluation errors.
541      unlet s:evalexpr
542      return
543    endif
544  elseif a:text[0] == '~'
545    let text = s:DecodeMessage(a:text[1:])
546  else
547    call s:CommOutput(a:channel, a:text)
548    return
549  endif
550
551  let curwinid = win_getid(winnr())
552  call win_gotoid(s:gdbwin)
553
554  " Add the output above the current prompt.
555  call append(line('$') - 1, text)
556  set modified
557
558  call win_gotoid(curwinid)
559endfunc
560
561" Decode a message from gdb.  quotedText starts with a ", return the text up
562" to the next ", unescaping characters.
563func s:DecodeMessage(quotedText)
564  if a:quotedText[0] != '"'
565    echoerr 'DecodeMessage(): missing quote in ' . a:quotedText
566    return
567  endif
568  let result = ''
569  let i = 1
570  while a:quotedText[i] != '"' && i < len(a:quotedText)
571    if a:quotedText[i] == '\'
572      let i += 1
573      if a:quotedText[i] == 'n'
574	" drop \n
575	let i += 1
576	continue
577      elseif a:quotedText[i] == 't'
578	" append \t
579	let i += 1
580	let result .= "\t"
581	continue
582      endif
583    endif
584    let result .= a:quotedText[i]
585    let i += 1
586  endwhile
587  return result
588endfunc
589
590" Extract the "name" value from a gdb message with fullname="name".
591func s:GetFullname(msg)
592  if a:msg !~ 'fullname'
593    return ''
594  endif
595  let name = s:DecodeMessage(substitute(a:msg, '.*fullname=', '', ''))
596  if has('win32') && name =~ ':\\\\'
597    " sometimes the name arrives double-escaped
598    let name = substitute(name, '\\\\', '\\', 'g')
599  endif
600  return name
601endfunc
602
603" Extract the "addr" value from a gdb message with addr="0x0001234".
604func s:GetAsmAddr(msg)
605  if a:msg !~ 'addr='
606    return ''
607  endif
608  let addr = s:DecodeMessage(substitute(a:msg, '.*addr=', '', ''))
609  return addr
610endfunc
611
612func s:EndTermDebug(job, status)
613  if exists('#User#TermdebugStopPre')
614    doauto <nomodeline> User TermdebugStopPre
615  endif
616
617  exe 'bwipe! ' . s:commbuf
618  unlet s:gdbwin
619
620  call s:EndDebugCommon()
621endfunc
622
623func s:EndDebugCommon()
624  let curwinid = win_getid(winnr())
625
626  if exists('s:ptybuf') && s:ptybuf
627    exe 'bwipe! ' . s:ptybuf
628  endif
629
630  " Restore 'signcolumn' in all buffers for which it was set.
631  call win_gotoid(s:sourcewin)
632  let was_buf = bufnr()
633  for bufnr in s:signcolumn_buflist
634    if bufexists(bufnr)
635      exe bufnr .. "buf"
636      if exists('b:save_signcolumn')
637	let &signcolumn = b:save_signcolumn
638	unlet b:save_signcolumn
639      endif
640    endif
641  endfor
642  exe was_buf .. "buf"
643
644  call s:DeleteCommands()
645
646  call win_gotoid(curwinid)
647
648  if s:save_columns > 0
649    let &columns = s:save_columns
650  endif
651
652  if has("balloon_eval") || has("balloon_eval_term")
653    set balloonexpr=
654    if has("balloon_eval")
655      set noballooneval
656    endif
657    if has("balloon_eval_term")
658      set noballoonevalterm
659    endif
660  endif
661
662  if exists('#User#TermdebugStopPost')
663    doauto <nomodeline> User TermdebugStopPost
664  endif
665
666  au! TermDebug
667endfunc
668
669func s:EndPromptDebug(job, status)
670  if exists('#User#TermdebugStopPre')
671    doauto <nomodeline> User TermdebugStopPre
672  endif
673
674  let curwinid = win_getid(winnr())
675  call win_gotoid(s:gdbwin)
676  set nomodified
677  close
678  if curwinid != s:gdbwin
679    call win_gotoid(curwinid)
680  endif
681
682  call s:EndDebugCommon()
683  unlet s:gdbwin
684  call ch_log("Returning from EndPromptDebug()")
685endfunc
686
687" Disassembly window - added by Michael Sartain
688"
689" - CommOutput: disassemble $pc
690" - CommOutput: &"disassemble $pc\n"
691" - CommOutput: ~"Dump of assembler code for function main(int, char**):\n"
692" - CommOutput: ~"   0x0000555556466f69 <+0>:\tpush   rbp\n"
693" ...
694" - CommOutput: ~"   0x0000555556467cd0:\tpop    rbp\n"
695" - CommOutput: ~"   0x0000555556467cd1:\tret    \n"
696" - CommOutput: ~"End of assembler dump.\n"
697" - CommOutput: ^done
698
699" - CommOutput: disassemble $pc
700" - CommOutput: &"disassemble $pc\n"
701" - CommOutput: &"No function contains specified address.\n"
702" - CommOutput: ^error,msg="No function contains specified address."
703func s:HandleDisasmMsg(msg)
704  if a:msg =~ '^\^done'
705    let curwinid = win_getid(winnr())
706    if win_gotoid(s:asmwin)
707      silent normal! gg0"_dG
708      call setline(1, s:asm_lines)
709      set nomodified
710      set filetype=asm
711
712      let lnum = search('^' . s:asm_addr)
713      if lnum != 0
714        exe 'sign unplace ' . s:asm_id
715        exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC'
716      endif
717
718      call win_gotoid(curwinid)
719    endif
720
721    let s:parsing_disasm_msg = 0
722    let s:asm_lines = []
723  elseif a:msg =~ '^\^error,msg='
724    if s:parsing_disasm_msg == 1
725      " Disassemble call ran into an error. This can happen when gdb can't
726      " find the function frame address, so let's try to disassemble starting
727      " at current PC
728      call s:SendCommand('disassemble $pc,+100')
729    endif
730    let s:parsing_disasm_msg = 0
731  elseif a:msg =~ '\&\"disassemble \$pc'
732    if a:msg =~ '+100'
733      " This is our second disasm attempt
734      let s:parsing_disasm_msg = 2
735    endif
736  else
737    let value = substitute(a:msg, '^\~\"[ ]*', '', '')
738    let value = substitute(value, '^=>[ ]*', '', '')
739    let value = substitute(value, '\\n\"
740$', '', '')
741    let value = substitute(value, '\\n\"$', '', '')
742    let value = substitute(value, '
743', '', '')
744    let value = substitute(value, '\\t', ' ', 'g')
745
746    if value != '' || !empty(s:asm_lines)
747      call add(s:asm_lines, value)
748    endif
749  endif
750endfunc
751
752" Handle a message received from gdb on the GDB/MI interface.
753func s:CommOutput(chan, msg)
754  let msgs = split(a:msg, "\r")
755
756  for msg in msgs
757    " remove prefixed NL
758    if msg[0] == "\n"
759      let msg = msg[1:]
760    endif
761
762    if s:parsing_disasm_msg
763      call s:HandleDisasmMsg(msg)
764    elseif msg != ''
765      if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)'
766	call s:HandleCursor(msg)
767      elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,'
768	call s:HandleNewBreakpoint(msg)
769      elseif msg =~ '^=breakpoint-deleted,'
770	call s:HandleBreakpointDelete(msg)
771      elseif msg =~ '^=thread-group-started'
772	call s:HandleProgramRun(msg)
773      elseif msg =~ '^\^done,value='
774	call s:HandleEvaluate(msg)
775      elseif msg =~ '^\^error,msg='
776	call s:HandleError(msg)
777      elseif msg =~ '^disassemble'
778        let s:parsing_disasm_msg = 1
779        let s:asm_lines = []
780      endif
781    endif
782  endfor
783endfunc
784
785func s:GotoProgram()
786  if has('win32')
787    if executable('powershell')
788      call system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', s:pid))
789    endif
790  else
791    call win_gotoid(s:ptywin)
792  endif
793endfunc
794
795" Install commands in the current window to control the debugger.
796func s:InstallCommands()
797  let save_cpo = &cpo
798  set cpo&vim
799
800  command -nargs=? Break call s:SetBreakpoint(<q-args>)
801  command Clear call s:ClearBreakpoint()
802  command Step call s:SendCommand('-exec-step')
803  command Over call s:SendCommand('-exec-next')
804  command Finish call s:SendCommand('-exec-finish')
805  command -nargs=* Run call s:Run(<q-args>)
806  command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>)
807  command Stop call s:SendCommand('-exec-interrupt')
808
809  " using -exec-continue results in CTRL-C in gdb window not working
810  if s:way == 'prompt'
811    command Continue call s:SendCommand('continue')
812  else
813    command Continue call term_sendkeys(s:gdbbuf, "continue\r")
814  endif
815
816  command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
817  command Gdb call win_gotoid(s:gdbwin)
818  command Program call s:GotoProgram()
819  command Source call s:GotoSourcewinOrCreateIt()
820  command Asm call s:GotoAsmwinOrCreateIt()
821  command Winbar call s:InstallWinbar()
822
823  if !exists('g:termdebug_map_K') || g:termdebug_map_K
824    let s:k_map_saved = maparg('K', 'n', 0, 1)
825    nnoremap K :Evaluate<CR>
826  endif
827
828  if has('menu') && &mouse != ''
829    call s:InstallWinbar()
830
831    if !exists('g:termdebug_popup') || g:termdebug_popup != 0
832      let s:saved_mousemodel = &mousemodel
833      let &mousemodel = 'popup_setpos'
834      an 1.200 PopUp.-SEP3-	<Nop>
835      an 1.210 PopUp.Set\ breakpoint	:Break<CR>
836      an 1.220 PopUp.Clear\ breakpoint	:Clear<CR>
837      an 1.230 PopUp.Evaluate		:Evaluate<CR>
838    endif
839  endif
840
841  let &cpo = save_cpo
842endfunc
843
844let s:winbar_winids = []
845
846" Install the window toolbar in the current window.
847func s:InstallWinbar()
848  if has('menu') && &mouse != ''
849    nnoremenu WinBar.Step   :Step<CR>
850    nnoremenu WinBar.Next   :Over<CR>
851    nnoremenu WinBar.Finish :Finish<CR>
852    nnoremenu WinBar.Cont   :Continue<CR>
853    nnoremenu WinBar.Stop   :Stop<CR>
854    nnoremenu WinBar.Eval   :Evaluate<CR>
855    call add(s:winbar_winids, win_getid(winnr()))
856  endif
857endfunc
858
859" Delete installed debugger commands in the current window.
860func s:DeleteCommands()
861  delcommand Break
862  delcommand Clear
863  delcommand Step
864  delcommand Over
865  delcommand Finish
866  delcommand Run
867  delcommand Arguments
868  delcommand Stop
869  delcommand Continue
870  delcommand Evaluate
871  delcommand Gdb
872  delcommand Program
873  delcommand Source
874  delcommand Asm
875  delcommand Winbar
876
877  if exists('s:k_map_saved')
878    if empty(s:k_map_saved)
879      nunmap K
880    else
881      call mapset('n', 0, s:k_map_saved)
882    endif
883    unlet s:k_map_saved
884  endif
885
886  if has('menu')
887    " Remove the WinBar entries from all windows where it was added.
888    let curwinid = win_getid(winnr())
889    for winid in s:winbar_winids
890      if win_gotoid(winid)
891	aunmenu WinBar.Step
892	aunmenu WinBar.Next
893	aunmenu WinBar.Finish
894	aunmenu WinBar.Cont
895	aunmenu WinBar.Stop
896	aunmenu WinBar.Eval
897      endif
898    endfor
899    call win_gotoid(curwinid)
900    let s:winbar_winids = []
901
902    if exists('s:saved_mousemodel')
903      let &mousemodel = s:saved_mousemodel
904      unlet s:saved_mousemodel
905      aunmenu PopUp.-SEP3-
906      aunmenu PopUp.Set\ breakpoint
907      aunmenu PopUp.Clear\ breakpoint
908      aunmenu PopUp.Evaluate
909    endif
910  endif
911
912  exe 'sign unplace ' . s:pc_id
913  for [id, entries] in items(s:breakpoints)
914    for subid in keys(entries)
915      exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
916    endfor
917  endfor
918  unlet s:breakpoints
919  unlet s:breakpoint_locations
920
921  sign undefine debugPC
922  for val in s:BreakpointSigns
923    exe "sign undefine debugBreakpoint" . val
924  endfor
925  let s:BreakpointSigns = []
926endfunc
927
928" :Break - Set a breakpoint at the cursor position.
929func s:SetBreakpoint(at)
930  " Setting a breakpoint may not work while the program is running.
931  " Interrupt to make it work.
932  let do_continue = 0
933  if !s:stopped
934    let do_continue = 1
935    if s:way == 'prompt'
936      call s:PromptInterrupt()
937    else
938      call s:SendCommand('-exec-interrupt')
939    endif
940    sleep 10m
941  endif
942
943  " Use the fname:lnum format, older gdb can't handle --source.
944  let at = empty(a:at) ?
945        \ fnameescape(expand('%:p')) . ':' . line('.') : a:at
946  call s:SendCommand('-break-insert ' . at)
947  if do_continue
948    call s:SendCommand('-exec-continue')
949  endif
950endfunc
951
952" :Clear - Delete a breakpoint at the cursor position.
953func s:ClearBreakpoint()
954  let fname = fnameescape(expand('%:p'))
955  let lnum = line('.')
956  let bploc = printf('%s:%d', fname, lnum)
957  if has_key(s:breakpoint_locations, bploc)
958    let idx = 0
959    for id in s:breakpoint_locations[bploc]
960      if has_key(s:breakpoints, id)
961	" Assume this always works, the reply is simply "^done".
962	call s:SendCommand('-break-delete ' . id)
963	for subid in keys(s:breakpoints[id])
964	  exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
965	endfor
966	unlet s:breakpoints[id]
967	unlet s:breakpoint_locations[bploc][idx]
968	break
969      else
970	let idx += 1
971      endif
972    endfor
973    if empty(s:breakpoint_locations[bploc])
974      unlet s:breakpoint_locations[bploc]
975    endif
976  endif
977endfunc
978
979func s:Run(args)
980  if a:args != ''
981    call s:SendCommand('-exec-arguments ' . a:args)
982  endif
983  call s:SendCommand('-exec-run')
984endfunc
985
986func s:SendEval(expr)
987  call s:SendCommand('-data-evaluate-expression "' . a:expr . '"')
988  let s:evalexpr = a:expr
989endfunc
990
991" :Evaluate - evaluate what is under the cursor
992func s:Evaluate(range, arg)
993  if a:arg != ''
994    let expr = a:arg
995  elseif a:range == 2
996    let pos = getcurpos()
997    let reg = getreg('v', 1, 1)
998    let regt = getregtype('v')
999    normal! gv"vy
1000    let expr = @v
1001    call setpos('.', pos)
1002    call setreg('v', reg, regt)
1003  else
1004    let expr = expand('<cexpr>')
1005  endif
1006  let s:ignoreEvalError = 0
1007  call s:SendEval(expr)
1008endfunc
1009
1010let s:ignoreEvalError = 0
1011let s:evalFromBalloonExpr = 0
1012
1013" Handle the result of data-evaluate-expression
1014func s:HandleEvaluate(msg)
1015  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
1016  let value = substitute(value, '\\"', '"', 'g')
1017  if s:evalFromBalloonExpr
1018    if s:evalFromBalloonExprResult == ''
1019      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
1020    else
1021      let s:evalFromBalloonExprResult .= ' = ' . value
1022    endif
1023    call balloon_show(s:evalFromBalloonExprResult)
1024  else
1025    echomsg '"' . s:evalexpr . '": ' . value
1026  endif
1027
1028  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
1029    " Looks like a pointer, also display what it points to.
1030    let s:ignoreEvalError = 1
1031    call s:SendEval('*' . s:evalexpr)
1032  else
1033    let s:evalFromBalloonExpr = 0
1034  endif
1035endfunc
1036
1037" Show a balloon with information of the variable under the mouse pointer,
1038" if there is any.
1039func TermDebugBalloonExpr()
1040  if v:beval_winid != s:sourcewin
1041    return ''
1042  endif
1043  if !s:stopped
1044    " Only evaluate when stopped, otherwise setting a breakpoint using the
1045    " mouse triggers a balloon.
1046    return ''
1047  endif
1048  let s:evalFromBalloonExpr = 1
1049  let s:evalFromBalloonExprResult = ''
1050  let s:ignoreEvalError = 1
1051  call s:SendEval(v:beval_text)
1052  return ''
1053endfunc
1054
1055" Handle an error.
1056func s:HandleError(msg)
1057  if s:ignoreEvalError
1058    " Result of s:SendEval() failed, ignore.
1059    let s:ignoreEvalError = 0
1060    let s:evalFromBalloonExpr = 0
1061    return
1062  endif
1063  echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
1064endfunc
1065
1066func s:GotoSourcewinOrCreateIt()
1067  if !win_gotoid(s:sourcewin)
1068    new
1069    let s:sourcewin = win_getid(winnr())
1070    call s:InstallWinbar()
1071  endif
1072endfunc
1073
1074func s:GotoAsmwinOrCreateIt()
1075  if !win_gotoid(s:asmwin)
1076    if win_gotoid(s:sourcewin)
1077      exe 'rightbelow new'
1078    else
1079      exe 'new'
1080    endif
1081
1082    let s:asmwin = win_getid(winnr())
1083
1084    setlocal nowrap
1085    setlocal number
1086    setlocal noswapfile
1087    setlocal buftype=nofile
1088
1089    let asmbuf = bufnr('Termdebug-asm-listing')
1090    if asmbuf > 0
1091      exe 'buffer' . asmbuf
1092    else
1093      exe 'file Termdebug-asm-listing'
1094    endif
1095
1096    if exists('g:termdebug_disasm_window')
1097      if g:termdebug_disasm_window > 1
1098        exe 'resize ' . g:termdebug_disasm_window
1099      endif
1100    endif
1101  endif
1102
1103  if s:asm_addr != ''
1104    let lnum = search('^' . s:asm_addr)
1105    if lnum == 0
1106      if s:stopped
1107        call s:SendCommand('disassemble $pc')
1108      endif
1109    else
1110      exe 'sign unplace ' . s:asm_id
1111      exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC'
1112    endif
1113  endif
1114endfunc
1115
1116" Handle stopping and running message from gdb.
1117" Will update the sign that shows the current position.
1118func s:HandleCursor(msg)
1119  let wid = win_getid(winnr())
1120
1121  if a:msg =~ '^\*stopped'
1122    call ch_log('program stopped')
1123    let s:stopped = 1
1124  elseif a:msg =~ '^\*running'
1125    call ch_log('program running')
1126    let s:stopped = 0
1127  endif
1128
1129  if a:msg =~ 'fullname='
1130    let fname = s:GetFullname(a:msg)
1131  else
1132    let fname = ''
1133  endif
1134
1135  if a:msg =~ 'addr='
1136    let asm_addr = s:GetAsmAddr(a:msg)
1137    if asm_addr != ''
1138      let s:asm_addr = asm_addr
1139
1140      let curwinid = win_getid(winnr())
1141      if win_gotoid(s:asmwin)
1142        let lnum = search('^' . s:asm_addr)
1143        if lnum == 0
1144          call s:SendCommand('disassemble $pc')
1145        else
1146          exe 'sign unplace ' . s:asm_id
1147          exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC'
1148        endif
1149
1150        call win_gotoid(curwinid)
1151      endif
1152    endif
1153  endif
1154
1155  if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
1156    let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
1157    if lnum =~ '^[0-9]*$'
1158    call s:GotoSourcewinOrCreateIt()
1159      if expand('%:p') != fnamemodify(fname, ':p')
1160	if &modified
1161	  " TODO: find existing window
1162	  exe 'split ' . fnameescape(fname)
1163	  let s:sourcewin = win_getid(winnr())
1164	  call s:InstallWinbar()
1165	else
1166	  exe 'edit ' . fnameescape(fname)
1167	endif
1168      endif
1169      exe lnum
1170      exe 'sign unplace ' . s:pc_id
1171      exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC priority=110 file=' . fname
1172      if !exists('b:save_signcolumn')
1173	let b:save_signcolumn = &signcolumn
1174	call add(s:signcolumn_buflist, bufnr())
1175      endif
1176      setlocal signcolumn=yes
1177    endif
1178  elseif !s:stopped || fname != ''
1179    exe 'sign unplace ' . s:pc_id
1180  endif
1181
1182  call win_gotoid(wid)
1183endfunc
1184
1185let s:BreakpointSigns = []
1186
1187func s:CreateBreakpoint(id, subid)
1188  let nr = printf('%d.%d', a:id, a:subid)
1189  if index(s:BreakpointSigns, nr) == -1
1190    call add(s:BreakpointSigns, nr)
1191    exe "sign define debugBreakpoint" . nr . " text=" . substitute(nr, '\..*', '', '') . " texthl=debugBreakpoint"
1192  endif
1193endfunc
1194
1195func! s:SplitMsg(s)
1196  return split(a:s, '{.\{-}}\zs')
1197endfunction
1198
1199" Handle setting a breakpoint
1200" Will update the sign that shows the breakpoint
1201func s:HandleNewBreakpoint(msg)
1202  if a:msg !~ 'fullname='
1203    " a watch does not have a file name
1204    return
1205  endif
1206  for msg in s:SplitMsg(a:msg)
1207    let fname = s:GetFullname(msg)
1208    if empty(fname)
1209      continue
1210    endif
1211    let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '')
1212    if empty(nr)
1213      return
1214    endif
1215
1216    " If "nr" is 123 it becomes "123.0" and subid is "0".
1217    " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded.
1218    let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0')
1219    call s:CreateBreakpoint(id, subid)
1220
1221    if has_key(s:breakpoints, id)
1222      let entries = s:breakpoints[id]
1223    else
1224      let entries = {}
1225      let s:breakpoints[id] = entries
1226    endif
1227    if has_key(entries, subid)
1228      let entry = entries[subid]
1229    else
1230      let entry = {}
1231      let entries[subid] = entry
1232    endif
1233
1234    let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '')
1235    let entry['fname'] = fname
1236    let entry['lnum'] = lnum
1237
1238    let bploc = printf('%s:%d', fname, lnum)
1239    if !has_key(s:breakpoint_locations, bploc)
1240      let s:breakpoint_locations[bploc] = []
1241    endif
1242    let s:breakpoint_locations[bploc] += [id]
1243
1244    if bufloaded(fname)
1245      call s:PlaceSign(id, subid, entry)
1246    endif
1247  endfor
1248endfunc
1249
1250func s:PlaceSign(id, subid, entry)
1251  let nr = printf('%d.%d', a:id, a:subid)
1252  exe 'sign place ' . s:Breakpoint2SignNumber(a:id, a:subid) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . nr . ' priority=110 file=' . a:entry['fname']
1253  let a:entry['placed'] = 1
1254endfunc
1255
1256" Handle deleting a breakpoint
1257" Will remove the sign that shows the breakpoint
1258func s:HandleBreakpointDelete(msg)
1259  let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
1260  if empty(id)
1261    return
1262  endif
1263  if has_key(s:breakpoints, id)
1264    for [subid, entry] in items(s:breakpoints[id])
1265      if has_key(entry, 'placed')
1266	exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
1267	unlet entry['placed']
1268      endif
1269    endfor
1270    unlet s:breakpoints[id]
1271  endif
1272endfunc
1273
1274" Handle the debugged program starting to run.
1275" Will store the process ID in s:pid
1276func s:HandleProgramRun(msg)
1277  let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0
1278  if nr == 0
1279    return
1280  endif
1281  let s:pid = nr
1282  call ch_log('Detected process ID: ' . s:pid)
1283endfunc
1284
1285" Handle a BufRead autocommand event: place any signs.
1286func s:BufRead()
1287  let fname = expand('<afile>:p')
1288  for [id, entries] in items(s:breakpoints)
1289    for [subid, entry] in items(entries)
1290      if entry['fname'] == fname
1291	call s:PlaceSign(id, subid, entry)
1292      endif
1293    endfor
1294  endfor
1295endfunc
1296
1297" Handle a BufUnloaded autocommand event: unplace any signs.
1298func s:BufUnloaded()
1299  let fname = expand('<afile>:p')
1300  for [id, entries] in items(s:breakpoints)
1301    for [subid, entry] in items(entries)
1302      if entry['fname'] == fname
1303	let entry['placed'] = 0
1304      endif
1305    endfor
1306  endfor
1307endfunc
1308
1309let &cpo = s:keepcpo
1310unlet s:keepcpo
1311