1" Debugger plugin using gdb.
2"
3" Author: Bram Moolenaar
4" Copyright: Vim license applies, see ":help license"
5" Last Change: 2021 Aug 06
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  call s:StartDebugCommon(a:dict)
331endfunc
332
333func s:StartDebug_prompt(dict)
334  " Open a window with a prompt buffer to run gdb in.
335  if s:vertical
336    vertical new
337  else
338    new
339  endif
340  let s:gdbwin = win_getid(winnr())
341  let s:promptbuf = bufnr('')
342  call prompt_setprompt(s:promptbuf, 'gdb> ')
343  set buftype=prompt
344  file gdb
345  call prompt_setcallback(s:promptbuf, function('s:PromptCallback'))
346  call prompt_setinterrupt(s:promptbuf, function('s:PromptInterrupt'))
347
348  if s:vertical
349    " Assuming the source code window will get a signcolumn, use two more
350    " columns for that, thus one less for the terminal window.
351    exe (&columns / 2 - 1) . "wincmd |"
352  endif
353
354  " Add -quiet to avoid the intro message causing a hit-enter prompt.
355  let gdb_args = get(a:dict, 'gdb_args', [])
356  let proc_args = get(a:dict, 'proc_args', [])
357
358  let cmd = [g:termdebugger, '-quiet', '--interpreter=mi2'] + gdb_args
359  call ch_log('executing "' . join(cmd) . '"')
360
361  let s:gdbjob = job_start(cmd, {
362	\ 'exit_cb': function('s:EndPromptDebug'),
363	\ 'out_cb': function('s:GdbOutCallback'),
364	\ })
365  if job_status(s:gdbjob) != "run"
366    echoerr 'Failed to start gdb'
367    exe 'bwipe! ' . s:promptbuf
368    return
369  endif
370  " Mark the buffer modified so that it's not easy to close.
371  set modified
372  let s:gdb_channel = job_getchannel(s:gdbjob)
373
374  " Interpret commands while the target is running.  This should usually only
375  " be exec-interrupt, since many commands don't work properly while the
376  " target is running.
377  call s:SendCommand('-gdb-set mi-async on')
378  " Older gdb uses a different command.
379  call s:SendCommand('-gdb-set target-async on')
380
381  let s:ptybuf = 0
382  if has('win32')
383    " MS-Windows: run in a new console window for maximum compatibility
384    call s:SendCommand('set new-console on')
385  elseif has('terminal')
386    " Unix: Run the debugged program in a terminal window.  Open it below the
387    " gdb window.
388    belowright let s:ptybuf = term_start('NONE', {
389	  \ 'term_name': 'debugged program',
390	  \ })
391    if s:ptybuf == 0
392      echoerr 'Failed to open the program terminal window'
393      call job_stop(s:gdbjob)
394      return
395    endif
396    let s:ptywin = win_getid(winnr())
397    let pty = job_info(term_getjob(s:ptybuf))['tty_out']
398    call s:SendCommand('tty ' . pty)
399
400    " Since GDB runs in a prompt window, the environment has not been set to
401    " match a terminal window, need to do that now.
402    call s:SendCommand('set env TERM = xterm-color')
403    call s:SendCommand('set env ROWS = ' . winheight(s:ptywin))
404    call s:SendCommand('set env LINES = ' . winheight(s:ptywin))
405    call s:SendCommand('set env COLUMNS = ' . winwidth(s:ptywin))
406    call s:SendCommand('set env COLORS = ' . &t_Co)
407    call s:SendCommand('set env VIM_TERMINAL = ' . v:version)
408  else
409    " TODO: open a new terminal get get the tty name, pass on to gdb
410    call s:SendCommand('show inferior-tty')
411  endif
412  call s:SendCommand('set print pretty on')
413  call s:SendCommand('set breakpoint pending on')
414  " Disable pagination, it causes everything to stop at the gdb
415  call s:SendCommand('set pagination off')
416
417  " Set arguments to be run
418  if len(proc_args)
419    call s:SendCommand('set args ' . join(proc_args))
420  endif
421
422  call s:StartDebugCommon(a:dict)
423  startinsert
424endfunc
425
426func s:StartDebugCommon(dict)
427  " Sign used to highlight the line where the program has stopped.
428  " There can be only one.
429  sign define debugPC linehl=debugPC
430
431  " Install debugger commands in the text window.
432  call win_gotoid(s:sourcewin)
433  call s:InstallCommands()
434  call win_gotoid(s:gdbwin)
435
436  " Enable showing a balloon with eval info
437  if has("balloon_eval") || has("balloon_eval_term")
438    set balloonexpr=TermDebugBalloonExpr()
439    if has("balloon_eval")
440      set ballooneval
441    endif
442    if has("balloon_eval_term")
443      set balloonevalterm
444    endif
445  endif
446
447  " Contains breakpoints that have been placed, key is a string with the GDB
448  " breakpoint number.
449  " Each entry is a dict, containing the sub-breakpoints.  Key is the subid.
450  " For a breakpoint that is just a number the subid is zero.
451  " For a breakpoint "123.4" the id is "123" and subid is "4".
452  " Example, when breakpoint "44", "123", "123.1" and "123.2" exist:
453  " {'44': {'0': entry}, '123': {'0': entry, '1': entry, '2': entry}}
454  let s:breakpoints = {}
455
456  " Contains breakpoints by file/lnum.  The key is "fname:lnum".
457  " Each entry is a list of breakpoint IDs at that position.
458  let s:breakpoint_locations = {}
459
460  augroup TermDebug
461    au BufRead * call s:BufRead()
462    au BufUnload * call s:BufUnloaded()
463    au OptionSet background call s:Highlight(0, v:option_old, v:option_new)
464  augroup END
465
466  " Run the command if the bang attribute was given and got to the debug
467  " window.
468  if get(a:dict, 'bang', 0)
469    call s:SendCommand('-exec-run')
470    call win_gotoid(s:ptywin)
471  endif
472endfunc
473
474" Send a command to gdb.  "cmd" is the string without line terminator.
475func s:SendCommand(cmd)
476  call ch_log('sending to gdb: ' . a:cmd)
477  if s:way == 'prompt'
478    call ch_sendraw(s:gdb_channel, a:cmd . "\n")
479  else
480    call term_sendkeys(s:commbuf, a:cmd . "\r")
481  endif
482endfunc
483
484" This is global so that a user can create their mappings with this.
485func TermDebugSendCommand(cmd)
486  if s:way == 'prompt'
487    call ch_sendraw(s:gdb_channel, a:cmd . "\n")
488  else
489    let do_continue = 0
490    if !s:stopped
491      let do_continue = 1
492      call s:SendCommand('-exec-interrupt')
493      sleep 10m
494    endif
495    call term_sendkeys(s:gdbbuf, a:cmd . "\r")
496    if do_continue
497      Continue
498    endif
499  endif
500endfunc
501
502" Function called when entering a line in the prompt buffer.
503func s:PromptCallback(text)
504  call s:SendCommand(a:text)
505endfunc
506
507" Function called when pressing CTRL-C in the prompt buffer and when placing a
508" breakpoint.
509func s:PromptInterrupt()
510  call ch_log('Interrupting gdb')
511  if has('win32')
512    " Using job_stop() does not work on MS-Windows, need to send SIGTRAP to
513    " the debugger program so that gdb responds again.
514    if s:pid == 0
515      echoerr 'Cannot interrupt gdb, did not find a process ID'
516    else
517      call debugbreak(s:pid)
518    endif
519  else
520    call job_stop(s:gdbjob, 'int')
521  endif
522endfunc
523
524" Function called when gdb outputs text.
525func s:GdbOutCallback(channel, text)
526  call ch_log('received from gdb: ' . a:text)
527
528  " Drop the gdb prompt, we have our own.
529  " Drop status and echo'd commands.
530  if a:text == '(gdb) ' || a:text == '^done' || a:text[0] == '&'
531    return
532  endif
533  if a:text =~ '^^error,msg='
534    let text = s:DecodeMessage(a:text[11:])
535    if exists('s:evalexpr') && text =~ 'A syntax error in expression, near\|No symbol .* in current context'
536      " Silently drop evaluation errors.
537      unlet s:evalexpr
538      return
539    endif
540  elseif a:text[0] == '~'
541    let text = s:DecodeMessage(a:text[1:])
542  else
543    call s:CommOutput(a:channel, a:text)
544    return
545  endif
546
547  let curwinid = win_getid(winnr())
548  call win_gotoid(s:gdbwin)
549
550  " Add the output above the current prompt.
551  call append(line('$') - 1, text)
552  set modified
553
554  call win_gotoid(curwinid)
555endfunc
556
557" Decode a message from gdb.  quotedText starts with a ", return the text up
558" to the next ", unescaping characters.
559func s:DecodeMessage(quotedText)
560  if a:quotedText[0] != '"'
561    echoerr 'DecodeMessage(): missing quote in ' . a:quotedText
562    return
563  endif
564  let result = ''
565  let i = 1
566  while a:quotedText[i] != '"' && i < len(a:quotedText)
567    if a:quotedText[i] == '\'
568      let i += 1
569      if a:quotedText[i] == 'n'
570	" drop \n
571	let i += 1
572	continue
573      elseif a:quotedText[i] == 't'
574	" append \t
575	let i += 1
576	let result .= "\t"
577	continue
578      endif
579    endif
580    let result .= a:quotedText[i]
581    let i += 1
582  endwhile
583  return result
584endfunc
585
586" Extract the "name" value from a gdb message with fullname="name".
587func s:GetFullname(msg)
588  if a:msg !~ 'fullname'
589    return ''
590  endif
591  let name = s:DecodeMessage(substitute(a:msg, '.*fullname=', '', ''))
592  if has('win32') && name =~ ':\\\\'
593    " sometimes the name arrives double-escaped
594    let name = substitute(name, '\\\\', '\\', 'g')
595  endif
596  return name
597endfunc
598
599" Extract the "addr" value from a gdb message with addr="0x0001234".
600func s:GetAsmAddr(msg)
601  if a:msg !~ 'addr='
602    return ''
603  endif
604  let addr = s:DecodeMessage(substitute(a:msg, '.*addr=', '', ''))
605  return addr
606endfunc
607
608func s:EndTermDebug(job, status)
609  if exists('#User#TermdebugStopPre')
610    doauto <nomodeline> User TermdebugStopPre
611  endif
612
613  exe 'bwipe! ' . s:commbuf
614  unlet s:gdbwin
615
616  call s:EndDebugCommon()
617endfunc
618
619func s:EndDebugCommon()
620  let curwinid = win_getid(winnr())
621
622  if exists('s:ptybuf') && s:ptybuf
623    exe 'bwipe! ' . s:ptybuf
624  endif
625
626  " Restore 'signcolumn' in all buffers for which it was set.
627  call win_gotoid(s:sourcewin)
628  let was_buf = bufnr()
629  for bufnr in s:signcolumn_buflist
630    if bufexists(bufnr)
631      exe bufnr .. "buf"
632      if exists('b:save_signcolumn')
633	let &signcolumn = b:save_signcolumn
634	unlet b:save_signcolumn
635      endif
636    endif
637  endfor
638  exe was_buf .. "buf"
639
640  call s:DeleteCommands()
641
642  call win_gotoid(curwinid)
643
644  if s:save_columns > 0
645    let &columns = s:save_columns
646  endif
647
648  if has("balloon_eval") || has("balloon_eval_term")
649    set balloonexpr=
650    if has("balloon_eval")
651      set noballooneval
652    endif
653    if has("balloon_eval_term")
654      set noballoonevalterm
655    endif
656  endif
657
658  if exists('#User#TermdebugStopPost')
659    doauto <nomodeline> User TermdebugStopPost
660  endif
661
662  au! TermDebug
663endfunc
664
665func s:EndPromptDebug(job, status)
666  if exists('#User#TermdebugStopPre')
667    doauto <nomodeline> User TermdebugStopPre
668  endif
669
670  let curwinid = win_getid(winnr())
671  call win_gotoid(s:gdbwin)
672  set nomodified
673  close
674  if curwinid != s:gdbwin
675    call win_gotoid(curwinid)
676  endif
677
678  call s:EndDebugCommon()
679  unlet s:gdbwin
680  call ch_log("Returning from EndPromptDebug()")
681endfunc
682
683" Disassembly window - added by Michael Sartain
684"
685" - CommOutput: disassemble $pc
686" - CommOutput: &"disassemble $pc\n"
687" - CommOutput: ~"Dump of assembler code for function main(int, char**):\n"
688" - CommOutput: ~"   0x0000555556466f69 <+0>:\tpush   rbp\n"
689" ...
690" - CommOutput: ~"   0x0000555556467cd0:\tpop    rbp\n"
691" - CommOutput: ~"   0x0000555556467cd1:\tret    \n"
692" - CommOutput: ~"End of assembler dump.\n"
693" - CommOutput: ^done
694
695" - CommOutput: disassemble $pc
696" - CommOutput: &"disassemble $pc\n"
697" - CommOutput: &"No function contains specified address.\n"
698" - CommOutput: ^error,msg="No function contains specified address."
699func s:HandleDisasmMsg(msg)
700  if a:msg =~ '^\^done'
701    let curwinid = win_getid(winnr())
702    if win_gotoid(s:asmwin)
703      silent normal! gg0"_dG
704      call setline(1, s:asm_lines)
705      set nomodified
706      set filetype=asm
707
708      let lnum = search('^' . s:asm_addr)
709      if lnum != 0
710        exe 'sign unplace ' . s:asm_id
711        exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC'
712      endif
713
714      call win_gotoid(curwinid)
715    endif
716
717    let s:parsing_disasm_msg = 0
718    let s:asm_lines = []
719  elseif a:msg =~ '^\^error,msg='
720    if s:parsing_disasm_msg == 1
721      " Disassemble call ran into an error. This can happen when gdb can't
722      " find the function frame address, so let's try to disassemble starting
723      " at current PC
724      call s:SendCommand('disassemble $pc,+100')
725    endif
726    let s:parsing_disasm_msg = 0
727  elseif a:msg =~ '\&\"disassemble \$pc'
728    if a:msg =~ '+100'
729      " This is our second disasm attempt
730      let s:parsing_disasm_msg = 2
731    endif
732  else
733    let value = substitute(a:msg, '^\~\"[ ]*', '', '')
734    let value = substitute(value, '^=>[ ]*', '', '')
735    let value = substitute(value, '\\n\"
736$', '', '')
737    let value = substitute(value, '\\n\"$', '', '')
738    let value = substitute(value, '
739', '', '')
740    let value = substitute(value, '\\t', ' ', 'g')
741
742    if value != '' || !empty(s:asm_lines)
743      call add(s:asm_lines, value)
744    endif
745  endif
746endfunc
747
748" Handle a message received from gdb on the GDB/MI interface.
749func s:CommOutput(chan, msg)
750  let msgs = split(a:msg, "\r")
751
752  for msg in msgs
753    " remove prefixed NL
754    if msg[0] == "\n"
755      let msg = msg[1:]
756    endif
757
758    if s:parsing_disasm_msg
759      call s:HandleDisasmMsg(msg)
760    elseif msg != ''
761      if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)'
762	call s:HandleCursor(msg)
763      elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,'
764	call s:HandleNewBreakpoint(msg)
765      elseif msg =~ '^=breakpoint-deleted,'
766	call s:HandleBreakpointDelete(msg)
767      elseif msg =~ '^=thread-group-started'
768	call s:HandleProgramRun(msg)
769      elseif msg =~ '^\^done,value='
770	call s:HandleEvaluate(msg)
771      elseif msg =~ '^\^error,msg='
772	call s:HandleError(msg)
773      elseif msg =~ '^disassemble'
774        let s:parsing_disasm_msg = 1
775        let s:asm_lines = []
776      endif
777    endif
778  endfor
779endfunc
780
781func s:GotoProgram()
782  if has('win32')
783    if executable('powershell')
784      call system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', s:pid))
785    endif
786  else
787    call win_gotoid(s:ptywin)
788  endif
789endfunc
790
791" Install commands in the current window to control the debugger.
792func s:InstallCommands()
793  let save_cpo = &cpo
794  set cpo&vim
795
796  command -nargs=? Break call s:SetBreakpoint(<q-args>)
797  command Clear call s:ClearBreakpoint()
798  command Step call s:SendCommand('-exec-step')
799  command Over call s:SendCommand('-exec-next')
800  command Finish call s:SendCommand('-exec-finish')
801  command -nargs=* Run call s:Run(<q-args>)
802  command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>)
803  command Stop call s:SendCommand('-exec-interrupt')
804
805  " using -exec-continue results in CTRL-C in gdb window not working
806  if s:way == 'prompt'
807    command Continue call s:SendCommand('continue')
808  else
809    command Continue call term_sendkeys(s:gdbbuf, "continue\r")
810  endif
811
812  command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
813  command Gdb call win_gotoid(s:gdbwin)
814  command Program call s:GotoProgram()
815  command Source call s:GotoSourcewinOrCreateIt()
816  command Asm call s:GotoAsmwinOrCreateIt()
817  command Winbar call s:InstallWinbar()
818
819  if !exists('g:termdebug_map_K') || g:termdebug_map_K
820    let s:k_map_saved = maparg('K', 'n', 0, 1)
821    nnoremap K :Evaluate<CR>
822  endif
823
824  if has('menu') && &mouse != ''
825    call s:InstallWinbar()
826
827    if !exists('g:termdebug_popup') || g:termdebug_popup != 0
828      let s:saved_mousemodel = &mousemodel
829      let &mousemodel = 'popup_setpos'
830      an 1.200 PopUp.-SEP3-	<Nop>
831      an 1.210 PopUp.Set\ breakpoint	:Break<CR>
832      an 1.220 PopUp.Clear\ breakpoint	:Clear<CR>
833      an 1.230 PopUp.Evaluate		:Evaluate<CR>
834    endif
835  endif
836
837  let &cpo = save_cpo
838endfunc
839
840let s:winbar_winids = []
841
842" Install the window toolbar in the current window.
843func s:InstallWinbar()
844  if has('menu') && &mouse != ''
845    nnoremenu WinBar.Step   :Step<CR>
846    nnoremenu WinBar.Next   :Over<CR>
847    nnoremenu WinBar.Finish :Finish<CR>
848    nnoremenu WinBar.Cont   :Continue<CR>
849    nnoremenu WinBar.Stop   :Stop<CR>
850    nnoremenu WinBar.Eval   :Evaluate<CR>
851    call add(s:winbar_winids, win_getid(winnr()))
852  endif
853endfunc
854
855" Delete installed debugger commands in the current window.
856func s:DeleteCommands()
857  delcommand Break
858  delcommand Clear
859  delcommand Step
860  delcommand Over
861  delcommand Finish
862  delcommand Run
863  delcommand Arguments
864  delcommand Stop
865  delcommand Continue
866  delcommand Evaluate
867  delcommand Gdb
868  delcommand Program
869  delcommand Source
870  delcommand Asm
871  delcommand Winbar
872
873  if exists('s:k_map_saved')
874    if empty(s:k_map_saved)
875      nunmap K
876    else
877      call mapset('n', 0, s:k_map_saved)
878    endif
879    unlet s:k_map_saved
880  endif
881
882  if has('menu')
883    " Remove the WinBar entries from all windows where it was added.
884    let curwinid = win_getid(winnr())
885    for winid in s:winbar_winids
886      if win_gotoid(winid)
887	aunmenu WinBar.Step
888	aunmenu WinBar.Next
889	aunmenu WinBar.Finish
890	aunmenu WinBar.Cont
891	aunmenu WinBar.Stop
892	aunmenu WinBar.Eval
893      endif
894    endfor
895    call win_gotoid(curwinid)
896    let s:winbar_winids = []
897
898    if exists('s:saved_mousemodel')
899      let &mousemodel = s:saved_mousemodel
900      unlet s:saved_mousemodel
901      aunmenu PopUp.-SEP3-
902      aunmenu PopUp.Set\ breakpoint
903      aunmenu PopUp.Clear\ breakpoint
904      aunmenu PopUp.Evaluate
905    endif
906  endif
907
908  exe 'sign unplace ' . s:pc_id
909  for [id, entries] in items(s:breakpoints)
910    for subid in keys(entries)
911      exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
912    endfor
913  endfor
914  unlet s:breakpoints
915  unlet s:breakpoint_locations
916
917  sign undefine debugPC
918  for val in s:BreakpointSigns
919    exe "sign undefine debugBreakpoint" . val
920  endfor
921  let s:BreakpointSigns = []
922endfunc
923
924" :Break - Set a breakpoint at the cursor position.
925func s:SetBreakpoint(at)
926  " Setting a breakpoint may not work while the program is running.
927  " Interrupt to make it work.
928  let do_continue = 0
929  if !s:stopped
930    let do_continue = 1
931    if s:way == 'prompt'
932      call s:PromptInterrupt()
933    else
934      call s:SendCommand('-exec-interrupt')
935    endif
936    sleep 10m
937  endif
938
939  " Use the fname:lnum format, older gdb can't handle --source.
940  let at = empty(a:at) ?
941        \ fnameescape(expand('%:p')) . ':' . line('.') : a:at
942  call s:SendCommand('-break-insert ' . at)
943  if do_continue
944    call s:SendCommand('-exec-continue')
945  endif
946endfunc
947
948" :Clear - Delete a breakpoint at the cursor position.
949func s:ClearBreakpoint()
950  let fname = fnameescape(expand('%:p'))
951  let lnum = line('.')
952  let bploc = printf('%s:%d', fname, lnum)
953  if has_key(s:breakpoint_locations, bploc)
954    let idx = 0
955    for id in s:breakpoint_locations[bploc]
956      if has_key(s:breakpoints, id)
957	" Assume this always works, the reply is simply "^done".
958	call s:SendCommand('-break-delete ' . id)
959	for subid in keys(s:breakpoints[id])
960	  exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
961	endfor
962	unlet s:breakpoints[id]
963	unlet s:breakpoint_locations[bploc][idx]
964	break
965      else
966	let idx += 1
967      endif
968    endfor
969    if empty(s:breakpoint_locations[bploc])
970      unlet s:breakpoint_locations[bploc]
971    endif
972  endif
973endfunc
974
975func s:Run(args)
976  if a:args != ''
977    call s:SendCommand('-exec-arguments ' . a:args)
978  endif
979  call s:SendCommand('-exec-run')
980endfunc
981
982func s:SendEval(expr)
983  call s:SendCommand('-data-evaluate-expression "' . a:expr . '"')
984  let s:evalexpr = a:expr
985endfunc
986
987" :Evaluate - evaluate what is under the cursor
988func s:Evaluate(range, arg)
989  if a:arg != ''
990    let expr = a:arg
991  elseif a:range == 2
992    let pos = getcurpos()
993    let reg = getreg('v', 1, 1)
994    let regt = getregtype('v')
995    normal! gv"vy
996    let expr = @v
997    call setpos('.', pos)
998    call setreg('v', reg, regt)
999  else
1000    let expr = expand('<cexpr>')
1001  endif
1002  let s:ignoreEvalError = 0
1003  call s:SendEval(expr)
1004endfunc
1005
1006let s:ignoreEvalError = 0
1007let s:evalFromBalloonExpr = 0
1008
1009" Handle the result of data-evaluate-expression
1010func s:HandleEvaluate(msg)
1011  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
1012  let value = substitute(value, '\\"', '"', 'g')
1013  if s:evalFromBalloonExpr
1014    if s:evalFromBalloonExprResult == ''
1015      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
1016    else
1017      let s:evalFromBalloonExprResult .= ' = ' . value
1018    endif
1019    call balloon_show(s:evalFromBalloonExprResult)
1020  else
1021    echomsg '"' . s:evalexpr . '": ' . value
1022  endif
1023
1024  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
1025    " Looks like a pointer, also display what it points to.
1026    let s:ignoreEvalError = 1
1027    call s:SendEval('*' . s:evalexpr)
1028  else
1029    let s:evalFromBalloonExpr = 0
1030  endif
1031endfunc
1032
1033" Show a balloon with information of the variable under the mouse pointer,
1034" if there is any.
1035func TermDebugBalloonExpr()
1036  if v:beval_winid != s:sourcewin
1037    return ''
1038  endif
1039  if !s:stopped
1040    " Only evaluate when stopped, otherwise setting a breakpoint using the
1041    " mouse triggers a balloon.
1042    return ''
1043  endif
1044  let s:evalFromBalloonExpr = 1
1045  let s:evalFromBalloonExprResult = ''
1046  let s:ignoreEvalError = 1
1047  call s:SendEval(v:beval_text)
1048  return ''
1049endfunc
1050
1051" Handle an error.
1052func s:HandleError(msg)
1053  if s:ignoreEvalError
1054    " Result of s:SendEval() failed, ignore.
1055    let s:ignoreEvalError = 0
1056    let s:evalFromBalloonExpr = 0
1057    return
1058  endif
1059  echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
1060endfunc
1061
1062func s:GotoSourcewinOrCreateIt()
1063  if !win_gotoid(s:sourcewin)
1064    new
1065    let s:sourcewin = win_getid(winnr())
1066    call s:InstallWinbar()
1067  endif
1068endfunc
1069
1070func s:GotoAsmwinOrCreateIt()
1071  if !win_gotoid(s:asmwin)
1072    if win_gotoid(s:sourcewin)
1073      exe 'rightbelow new'
1074    else
1075      exe 'new'
1076    endif
1077
1078    let s:asmwin = win_getid(winnr())
1079
1080    setlocal nowrap
1081    setlocal number
1082    setlocal noswapfile
1083    setlocal buftype=nofile
1084
1085    let asmbuf = bufnr('Termdebug-asm-listing')
1086    if asmbuf > 0
1087      exe 'buffer' . asmbuf
1088    else
1089      exe 'file Termdebug-asm-listing'
1090    endif
1091
1092    if exists('g:termdebug_disasm_window')
1093      if g:termdebug_disasm_window > 1
1094        exe 'resize ' . g:termdebug_disasm_window
1095      endif
1096    endif
1097  endif
1098
1099  if s:asm_addr != ''
1100    let lnum = search('^' . s:asm_addr)
1101    if lnum == 0
1102      if s:stopped
1103        call s:SendCommand('disassemble $pc')
1104      endif
1105    else
1106      exe 'sign unplace ' . s:asm_id
1107      exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC'
1108    endif
1109  endif
1110endfunc
1111
1112" Handle stopping and running message from gdb.
1113" Will update the sign that shows the current position.
1114func s:HandleCursor(msg)
1115  let wid = win_getid(winnr())
1116
1117  if a:msg =~ '^\*stopped'
1118    call ch_log('program stopped')
1119    let s:stopped = 1
1120  elseif a:msg =~ '^\*running'
1121    call ch_log('program running')
1122    let s:stopped = 0
1123  endif
1124
1125  if a:msg =~ 'fullname='
1126    let fname = s:GetFullname(a:msg)
1127  else
1128    let fname = ''
1129  endif
1130
1131  if a:msg =~ 'addr='
1132    let asm_addr = s:GetAsmAddr(a:msg)
1133    if asm_addr != ''
1134      let s:asm_addr = asm_addr
1135
1136      let curwinid = win_getid(winnr())
1137      if win_gotoid(s:asmwin)
1138        let lnum = search('^' . s:asm_addr)
1139        if lnum == 0
1140          call s:SendCommand('disassemble $pc')
1141        else
1142          exe 'sign unplace ' . s:asm_id
1143          exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC'
1144        endif
1145
1146        call win_gotoid(curwinid)
1147      endif
1148    endif
1149  endif
1150
1151  if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
1152    let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
1153    if lnum =~ '^[0-9]*$'
1154    call s:GotoSourcewinOrCreateIt()
1155      if expand('%:p') != fnamemodify(fname, ':p')
1156	if &modified
1157	  " TODO: find existing window
1158	  exe 'split ' . fnameescape(fname)
1159	  let s:sourcewin = win_getid(winnr())
1160	  call s:InstallWinbar()
1161	else
1162	  exe 'edit ' . fnameescape(fname)
1163	endif
1164      endif
1165      exe lnum
1166      exe 'sign unplace ' . s:pc_id
1167      exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC priority=110 file=' . fname
1168      if !exists('b:save_signcolumn')
1169	let b:save_signcolumn = &signcolumn
1170	call add(s:signcolumn_buflist, bufnr())
1171      endif
1172      setlocal signcolumn=yes
1173    endif
1174  elseif !s:stopped || fname != ''
1175    exe 'sign unplace ' . s:pc_id
1176  endif
1177
1178  call win_gotoid(wid)
1179endfunc
1180
1181let s:BreakpointSigns = []
1182
1183func s:CreateBreakpoint(id, subid)
1184  let nr = printf('%d.%d', a:id, a:subid)
1185  if index(s:BreakpointSigns, nr) == -1
1186    call add(s:BreakpointSigns, nr)
1187    exe "sign define debugBreakpoint" . nr . " text=" . substitute(nr, '\..*', '', '') . " texthl=debugBreakpoint"
1188  endif
1189endfunc
1190
1191func! s:SplitMsg(s)
1192  return split(a:s, '{.\{-}}\zs')
1193endfunction
1194
1195" Handle setting a breakpoint
1196" Will update the sign that shows the breakpoint
1197func s:HandleNewBreakpoint(msg)
1198  if a:msg !~ 'fullname='
1199    " a watch does not have a file name
1200    return
1201  endif
1202  for msg in s:SplitMsg(a:msg)
1203    let fname = s:GetFullname(msg)
1204    if empty(fname)
1205      continue
1206    endif
1207    let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '')
1208    if empty(nr)
1209      return
1210    endif
1211
1212    " If "nr" is 123 it becomes "123.0" and subid is "0".
1213    " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded.
1214    let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0')
1215    call s:CreateBreakpoint(id, subid)
1216
1217    if has_key(s:breakpoints, id)
1218      let entries = s:breakpoints[id]
1219    else
1220      let entries = {}
1221      let s:breakpoints[id] = entries
1222    endif
1223    if has_key(entries, subid)
1224      let entry = entries[subid]
1225    else
1226      let entry = {}
1227      let entries[subid] = entry
1228    endif
1229
1230    let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '')
1231    let entry['fname'] = fname
1232    let entry['lnum'] = lnum
1233
1234    let bploc = printf('%s:%d', fname, lnum)
1235    if !has_key(s:breakpoint_locations, bploc)
1236      let s:breakpoint_locations[bploc] = []
1237    endif
1238    let s:breakpoint_locations[bploc] += [id]
1239
1240    if bufloaded(fname)
1241      call s:PlaceSign(id, subid, entry)
1242    endif
1243  endfor
1244endfunc
1245
1246func s:PlaceSign(id, subid, entry)
1247  let nr = printf('%d.%d', a:id, a:subid)
1248  exe 'sign place ' . s:Breakpoint2SignNumber(a:id, a:subid) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . nr . ' priority=110 file=' . a:entry['fname']
1249  let a:entry['placed'] = 1
1250endfunc
1251
1252" Handle deleting a breakpoint
1253" Will remove the sign that shows the breakpoint
1254func s:HandleBreakpointDelete(msg)
1255  let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
1256  if empty(id)
1257    return
1258  endif
1259  if has_key(s:breakpoints, id)
1260    for [subid, entry] in items(s:breakpoints[id])
1261      if has_key(entry, 'placed')
1262	exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
1263	unlet entry['placed']
1264      endif
1265    endfor
1266    unlet s:breakpoints[id]
1267  endif
1268endfunc
1269
1270" Handle the debugged program starting to run.
1271" Will store the process ID in s:pid
1272func s:HandleProgramRun(msg)
1273  let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0
1274  if nr == 0
1275    return
1276  endif
1277  let s:pid = nr
1278  call ch_log('Detected process ID: ' . s:pid)
1279endfunc
1280
1281" Handle a BufRead autocommand event: place any signs.
1282func s:BufRead()
1283  let fname = expand('<afile>:p')
1284  for [id, entries] in items(s:breakpoints)
1285    for [subid, entry] in items(entries)
1286      if entry['fname'] == fname
1287	call s:PlaceSign(id, subid, entry)
1288      endif
1289    endfor
1290  endfor
1291endfunc
1292
1293" Handle a BufUnloaded autocommand event: unplace any signs.
1294func s:BufUnloaded()
1295  let fname = expand('<afile>:p')
1296  for [id, entries] in items(s:breakpoints)
1297    for [subid, entry] in items(entries)
1298      if entry['fname'] == fname
1299	let entry['placed'] = 0
1300      endif
1301    endfor
1302  endfor
1303endfunc
1304
1305let &cpo = s:keepcpo
1306unlet s:keepcpo
1307