1" Debugger plugin using gdb.
2"
3" Author: Bram Moolenaar
4" Copyright: Vim license applies, see ":help license"
5" Last Change: 2021 Nov 14
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\"\r$', '', '')
740    let value = substitute(value, '\\n\"$', '', '')
741    let value = substitute(value, '\r', '', '')
742    let value = substitute(value, '\\t', ' ', 'g')
743
744    if value != '' || !empty(s:asm_lines)
745      call add(s:asm_lines, value)
746    endif
747  endif
748endfunc
749
750" Handle a message received from gdb on the GDB/MI interface.
751func s:CommOutput(chan, msg)
752  let msgs = split(a:msg, "\r")
753
754  for msg in msgs
755    " remove prefixed NL
756    if msg[0] == "\n"
757      let msg = msg[1:]
758    endif
759
760    if s:parsing_disasm_msg
761      call s:HandleDisasmMsg(msg)
762    elseif msg != ''
763      if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)'
764	call s:HandleCursor(msg)
765      elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,'
766	call s:HandleNewBreakpoint(msg)
767      elseif msg =~ '^=breakpoint-deleted,'
768	call s:HandleBreakpointDelete(msg)
769      elseif msg =~ '^=thread-group-started'
770	call s:HandleProgramRun(msg)
771      elseif msg =~ '^\^done,value='
772	call s:HandleEvaluate(msg)
773      elseif msg =~ '^\^error,msg='
774	call s:HandleError(msg)
775      elseif msg =~ '^disassemble'
776        let s:parsing_disasm_msg = 1
777        let s:asm_lines = []
778      endif
779    endif
780  endfor
781endfunc
782
783func s:GotoProgram()
784  if has('win32')
785    if executable('powershell')
786      call system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', s:pid))
787    endif
788  else
789    call win_gotoid(s:ptywin)
790  endif
791endfunc
792
793" Install commands in the current window to control the debugger.
794func s:InstallCommands()
795  let save_cpo = &cpo
796  set cpo&vim
797
798  command -nargs=? Break call s:SetBreakpoint(<q-args>)
799  command Clear call s:ClearBreakpoint()
800  command Step call s:SendCommand('-exec-step')
801  command Over call s:SendCommand('-exec-next')
802  command Finish call s:SendCommand('-exec-finish')
803  command -nargs=* Run call s:Run(<q-args>)
804  command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>)
805  command Stop call s:SendCommand('-exec-interrupt')
806
807  " using -exec-continue results in CTRL-C in gdb window not working
808  if s:way == 'prompt'
809    command Continue call s:SendCommand('continue')
810  else
811    command Continue call term_sendkeys(s:gdbbuf, "continue\r")
812  endif
813
814  command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
815  command Gdb call win_gotoid(s:gdbwin)
816  command Program call s:GotoProgram()
817  command Source call s:GotoSourcewinOrCreateIt()
818  command Asm call s:GotoAsmwinOrCreateIt()
819  command Winbar call s:InstallWinbar()
820
821  if !exists('g:termdebug_map_K') || g:termdebug_map_K
822    let s:k_map_saved = maparg('K', 'n', 0, 1)
823    nnoremap K :Evaluate<CR>
824  endif
825
826  if has('menu') && &mouse != ''
827    call s:InstallWinbar()
828
829    if !exists('g:termdebug_popup') || g:termdebug_popup != 0
830      let s:saved_mousemodel = &mousemodel
831      let &mousemodel = 'popup_setpos'
832      an 1.200 PopUp.-SEP3-	<Nop>
833      an 1.210 PopUp.Set\ breakpoint	:Break<CR>
834      an 1.220 PopUp.Clear\ breakpoint	:Clear<CR>
835      an 1.230 PopUp.Evaluate		:Evaluate<CR>
836    endif
837  endif
838
839  let &cpo = save_cpo
840endfunc
841
842let s:winbar_winids = []
843
844" Install the window toolbar in the current window.
845func s:InstallWinbar()
846  if has('menu') && &mouse != ''
847    nnoremenu WinBar.Step   :Step<CR>
848    nnoremenu WinBar.Next   :Over<CR>
849    nnoremenu WinBar.Finish :Finish<CR>
850    nnoremenu WinBar.Cont   :Continue<CR>
851    nnoremenu WinBar.Stop   :Stop<CR>
852    nnoremenu WinBar.Eval   :Evaluate<CR>
853    call add(s:winbar_winids, win_getid(winnr()))
854  endif
855endfunc
856
857" Delete installed debugger commands in the current window.
858func s:DeleteCommands()
859  delcommand Break
860  delcommand Clear
861  delcommand Step
862  delcommand Over
863  delcommand Finish
864  delcommand Run
865  delcommand Arguments
866  delcommand Stop
867  delcommand Continue
868  delcommand Evaluate
869  delcommand Gdb
870  delcommand Program
871  delcommand Source
872  delcommand Asm
873  delcommand Winbar
874
875  if exists('s:k_map_saved')
876    if empty(s:k_map_saved)
877      nunmap K
878    else
879      call mapset('n', 0, s:k_map_saved)
880    endif
881    unlet s:k_map_saved
882  endif
883
884  if has('menu')
885    " Remove the WinBar entries from all windows where it was added.
886    let curwinid = win_getid(winnr())
887    for winid in s:winbar_winids
888      if win_gotoid(winid)
889	aunmenu WinBar.Step
890	aunmenu WinBar.Next
891	aunmenu WinBar.Finish
892	aunmenu WinBar.Cont
893	aunmenu WinBar.Stop
894	aunmenu WinBar.Eval
895      endif
896    endfor
897    call win_gotoid(curwinid)
898    let s:winbar_winids = []
899
900    if exists('s:saved_mousemodel')
901      let &mousemodel = s:saved_mousemodel
902      unlet s:saved_mousemodel
903      aunmenu PopUp.-SEP3-
904      aunmenu PopUp.Set\ breakpoint
905      aunmenu PopUp.Clear\ breakpoint
906      aunmenu PopUp.Evaluate
907    endif
908  endif
909
910  exe 'sign unplace ' . s:pc_id
911  for [id, entries] in items(s:breakpoints)
912    for subid in keys(entries)
913      exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
914    endfor
915  endfor
916  unlet s:breakpoints
917  unlet s:breakpoint_locations
918
919  sign undefine debugPC
920  for val in s:BreakpointSigns
921    exe "sign undefine debugBreakpoint" . val
922  endfor
923  let s:BreakpointSigns = []
924endfunc
925
926" :Break - Set a breakpoint at the cursor position.
927func s:SetBreakpoint(at)
928  " Setting a breakpoint may not work while the program is running.
929  " Interrupt to make it work.
930  let do_continue = 0
931  if !s:stopped
932    let do_continue = 1
933    if s:way == 'prompt'
934      call s:PromptInterrupt()
935    else
936      call s:SendCommand('-exec-interrupt')
937    endif
938    sleep 10m
939  endif
940
941  " Use the fname:lnum format, older gdb can't handle --source.
942  let at = empty(a:at) ?
943        \ fnameescape(expand('%:p')) . ':' . line('.') : a:at
944  call s:SendCommand('-break-insert ' . at)
945  if do_continue
946    call s:SendCommand('-exec-continue')
947  endif
948endfunc
949
950" :Clear - Delete a breakpoint at the cursor position.
951func s:ClearBreakpoint()
952  let fname = fnameescape(expand('%:p'))
953  let lnum = line('.')
954  let bploc = printf('%s:%d', fname, lnum)
955  if has_key(s:breakpoint_locations, bploc)
956    let idx = 0
957    for id in s:breakpoint_locations[bploc]
958      if has_key(s:breakpoints, id)
959	" Assume this always works, the reply is simply "^done".
960	call s:SendCommand('-break-delete ' . id)
961	for subid in keys(s:breakpoints[id])
962	  exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
963	endfor
964	unlet s:breakpoints[id]
965	unlet s:breakpoint_locations[bploc][idx]
966	break
967      else
968	let idx += 1
969      endif
970    endfor
971    if empty(s:breakpoint_locations[bploc])
972      unlet s:breakpoint_locations[bploc]
973    endif
974  endif
975endfunc
976
977func s:Run(args)
978  if a:args != ''
979    call s:SendCommand('-exec-arguments ' . a:args)
980  endif
981  call s:SendCommand('-exec-run')
982endfunc
983
984func s:SendEval(expr)
985  " clean up expression that may got in because of range
986  " (newlines and surrounding spaces)
987  let expr = a:expr
988  if &filetype ==# 'cobol'
989    " extra cleanup for COBOL: _every: expression ends with a period,
990    " a trailing comma is ignored as it commonly separates multiple expr.
991    let expr = substitute(expr, '\..*', '', '')
992    let expr = substitute(expr, '[;\n]', ' ', 'g')
993    let expr = substitute(expr, ',*$', '', '')
994  else
995    let expr = substitute(expr, '\n', ' ', 'g')
996  endif
997  let expr = substitute(expr, '^ *\(.*\) *', '\1', '')
998
999  call s:SendCommand('-data-evaluate-expression "' . expr . '"')
1000  let s:evalexpr = expr
1001endfunc
1002
1003" :Evaluate - evaluate what is under the cursor
1004func s:Evaluate(range, arg)
1005  if a:arg != ''
1006    let expr = a:arg
1007  elseif a:range == 2
1008    let pos = getcurpos()
1009    let reg = getreg('v', 1, 1)
1010    let regt = getregtype('v')
1011    normal! gv"vy
1012    let expr = @v
1013    call setpos('.', pos)
1014    call setreg('v', reg, regt)
1015  else
1016    let expr = expand('<cexpr>')
1017  endif
1018  let s:ignoreEvalError = 0
1019  call s:SendEval(expr)
1020endfunc
1021
1022let s:ignoreEvalError = 0
1023let s:evalFromBalloonExpr = 0
1024
1025" Handle the result of data-evaluate-expression
1026func s:HandleEvaluate(msg)
1027  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
1028  let value = substitute(value, '\\"', '"', 'g')
1029  if s:evalFromBalloonExpr
1030    if s:evalFromBalloonExprResult == ''
1031      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
1032    else
1033      let s:evalFromBalloonExprResult .= ' = ' . value
1034    endif
1035    call balloon_show(s:evalFromBalloonExprResult)
1036  else
1037    echomsg '"' . s:evalexpr . '": ' . value
1038  endif
1039
1040  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
1041    " Looks like a pointer, also display what it points to.
1042    let s:ignoreEvalError = 1
1043    call s:SendEval('*' . s:evalexpr)
1044  else
1045    let s:evalFromBalloonExpr = 0
1046  endif
1047endfunc
1048
1049" Show a balloon with information of the variable under the mouse pointer,
1050" if there is any.
1051func TermDebugBalloonExpr()
1052  if v:beval_winid != s:sourcewin
1053    return ''
1054  endif
1055  if !s:stopped
1056    " Only evaluate when stopped, otherwise setting a breakpoint using the
1057    " mouse triggers a balloon.
1058    return ''
1059  endif
1060  let s:evalFromBalloonExpr = 1
1061  let s:evalFromBalloonExprResult = ''
1062  let s:ignoreEvalError = 1
1063  call s:SendEval(v:beval_text)
1064  return ''
1065endfunc
1066
1067" Handle an error.
1068func s:HandleError(msg)
1069  if s:ignoreEvalError
1070    " Result of s:SendEval() failed, ignore.
1071    let s:ignoreEvalError = 0
1072    let s:evalFromBalloonExpr = 0
1073    return
1074  endif
1075  let msgVal = substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
1076  echoerr substitute(msgVal, '\\"', '"', 'g')
1077endfunc
1078
1079func s:GotoSourcewinOrCreateIt()
1080  if !win_gotoid(s:sourcewin)
1081    new
1082    let s:sourcewin = win_getid(winnr())
1083    call s:InstallWinbar()
1084  endif
1085endfunc
1086
1087func s:GotoAsmwinOrCreateIt()
1088  if !win_gotoid(s:asmwin)
1089    if win_gotoid(s:sourcewin)
1090      exe 'rightbelow new'
1091    else
1092      exe 'new'
1093    endif
1094
1095    let s:asmwin = win_getid(winnr())
1096
1097    setlocal nowrap
1098    setlocal number
1099    setlocal noswapfile
1100    setlocal buftype=nofile
1101    setlocal modifiable
1102
1103    let asmbuf = bufnr('Termdebug-asm-listing')
1104    if asmbuf > 0
1105      exe 'buffer' . asmbuf
1106    else
1107      exe 'file Termdebug-asm-listing'
1108    endif
1109
1110    if exists('g:termdebug_disasm_window')
1111      if g:termdebug_disasm_window > 1
1112        exe 'resize ' . g:termdebug_disasm_window
1113      endif
1114    endif
1115  endif
1116
1117  if s:asm_addr != ''
1118    let lnum = search('^' . s:asm_addr)
1119    if lnum == 0
1120      if s:stopped
1121        call s:SendCommand('disassemble $pc')
1122      endif
1123    else
1124      exe 'sign unplace ' . s:asm_id
1125      exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC'
1126    endif
1127  endif
1128endfunc
1129
1130" Handle stopping and running message from gdb.
1131" Will update the sign that shows the current position.
1132func s:HandleCursor(msg)
1133  let wid = win_getid(winnr())
1134
1135  if a:msg =~ '^\*stopped'
1136    call ch_log('program stopped')
1137    let s:stopped = 1
1138  elseif a:msg =~ '^\*running'
1139    call ch_log('program running')
1140    let s:stopped = 0
1141  endif
1142
1143  if a:msg =~ 'fullname='
1144    let fname = s:GetFullname(a:msg)
1145  else
1146    let fname = ''
1147  endif
1148
1149  if a:msg =~ 'addr='
1150    let asm_addr = s:GetAsmAddr(a:msg)
1151    if asm_addr != ''
1152      let s:asm_addr = asm_addr
1153
1154      let curwinid = win_getid(winnr())
1155      if win_gotoid(s:asmwin)
1156        let lnum = search('^' . s:asm_addr)
1157        if lnum == 0
1158          call s:SendCommand('disassemble $pc')
1159        else
1160          exe 'sign unplace ' . s:asm_id
1161          exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC'
1162        endif
1163
1164        call win_gotoid(curwinid)
1165      endif
1166    endif
1167  endif
1168
1169  if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
1170    let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
1171    if lnum =~ '^[0-9]*$'
1172    call s:GotoSourcewinOrCreateIt()
1173      if expand('%:p') != fnamemodify(fname, ':p')
1174	if &modified
1175	  " TODO: find existing window
1176	  exe 'split ' . fnameescape(fname)
1177	  let s:sourcewin = win_getid(winnr())
1178	  call s:InstallWinbar()
1179	else
1180	  exe 'edit ' . fnameescape(fname)
1181	endif
1182      endif
1183      exe lnum
1184      normal! zv
1185      exe 'sign unplace ' . s:pc_id
1186      exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC priority=110 file=' . fname
1187      if !exists('b:save_signcolumn')
1188	let b:save_signcolumn = &signcolumn
1189	call add(s:signcolumn_buflist, bufnr())
1190      endif
1191      setlocal signcolumn=yes
1192    endif
1193  elseif !s:stopped || fname != ''
1194    exe 'sign unplace ' . s:pc_id
1195  endif
1196
1197  call win_gotoid(wid)
1198endfunc
1199
1200let s:BreakpointSigns = []
1201
1202func s:CreateBreakpoint(id, subid)
1203  let nr = printf('%d.%d', a:id, a:subid)
1204  if index(s:BreakpointSigns, nr) == -1
1205    call add(s:BreakpointSigns, nr)
1206    exe "sign define debugBreakpoint" . nr . " text=" . substitute(nr, '\..*', '', '') . " texthl=debugBreakpoint"
1207  endif
1208endfunc
1209
1210func! s:SplitMsg(s)
1211  return split(a:s, '{.\{-}}\zs')
1212endfunction
1213
1214" Handle setting a breakpoint
1215" Will update the sign that shows the breakpoint
1216func s:HandleNewBreakpoint(msg)
1217  if a:msg !~ 'fullname='
1218    " a watch does not have a file name
1219    return
1220  endif
1221  for msg in s:SplitMsg(a:msg)
1222    let fname = s:GetFullname(msg)
1223    if empty(fname)
1224      continue
1225    endif
1226    let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '')
1227    if empty(nr)
1228      return
1229    endif
1230
1231    " If "nr" is 123 it becomes "123.0" and subid is "0".
1232    " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded.
1233    let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0')
1234    call s:CreateBreakpoint(id, subid)
1235
1236    if has_key(s:breakpoints, id)
1237      let entries = s:breakpoints[id]
1238    else
1239      let entries = {}
1240      let s:breakpoints[id] = entries
1241    endif
1242    if has_key(entries, subid)
1243      let entry = entries[subid]
1244    else
1245      let entry = {}
1246      let entries[subid] = entry
1247    endif
1248
1249    let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '')
1250    let entry['fname'] = fname
1251    let entry['lnum'] = lnum
1252
1253    let bploc = printf('%s:%d', fname, lnum)
1254    if !has_key(s:breakpoint_locations, bploc)
1255      let s:breakpoint_locations[bploc] = []
1256    endif
1257    let s:breakpoint_locations[bploc] += [id]
1258
1259    if bufloaded(fname)
1260      call s:PlaceSign(id, subid, entry)
1261    endif
1262  endfor
1263endfunc
1264
1265func s:PlaceSign(id, subid, entry)
1266  let nr = printf('%d.%d', a:id, a:subid)
1267  exe 'sign place ' . s:Breakpoint2SignNumber(a:id, a:subid) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . nr . ' priority=110 file=' . a:entry['fname']
1268  let a:entry['placed'] = 1
1269endfunc
1270
1271" Handle deleting a breakpoint
1272" Will remove the sign that shows the breakpoint
1273func s:HandleBreakpointDelete(msg)
1274  let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
1275  if empty(id)
1276    return
1277  endif
1278  if has_key(s:breakpoints, id)
1279    for [subid, entry] in items(s:breakpoints[id])
1280      if has_key(entry, 'placed')
1281	exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
1282	unlet entry['placed']
1283      endif
1284    endfor
1285    unlet s:breakpoints[id]
1286  endif
1287endfunc
1288
1289" Handle the debugged program starting to run.
1290" Will store the process ID in s:pid
1291func s:HandleProgramRun(msg)
1292  let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0
1293  if nr == 0
1294    return
1295  endif
1296  let s:pid = nr
1297  call ch_log('Detected process ID: ' . s:pid)
1298endfunc
1299
1300" Handle a BufRead autocommand event: place any signs.
1301func s:BufRead()
1302  let fname = expand('<afile>:p')
1303  for [id, entries] in items(s:breakpoints)
1304    for [subid, entry] in items(entries)
1305      if entry['fname'] == fname
1306	call s:PlaceSign(id, subid, entry)
1307      endif
1308    endfor
1309  endfor
1310endfunc
1311
1312" Handle a BufUnloaded autocommand event: unplace any signs.
1313func s:BufUnloaded()
1314  let fname = expand('<afile>:p')
1315  for [id, entries] in items(s:breakpoints)
1316    for [subid, entry] in items(entries)
1317      if entry['fname'] == fname
1318	let entry['placed'] = 0
1319      endif
1320    endfor
1321  endfor
1322endfunc
1323
1324let &cpo = s:keepcpo
1325unlet s:keepcpo
1326