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