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