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