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