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