1" Debugger plugin using gdb.
2"
3" Author: Bram Moolenaar
4" Copyright: Vim license applies, see ":help license"
5" Last Change: 2020 Feb 19
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  " TODO: can the K mapping be restored?
661  nnoremap K :Evaluate<CR>
662
663  if has('menu') && &mouse != ''
664    call s:InstallWinbar()
665
666    if !exists('g:termdebug_popup') || g:termdebug_popup != 0
667      let s:saved_mousemodel = &mousemodel
668      let &mousemodel = 'popup_setpos'
669      an 1.200 PopUp.-SEP3-	<Nop>
670      an 1.210 PopUp.Set\ breakpoint	:Break<CR>
671      an 1.220 PopUp.Clear\ breakpoint	:Clear<CR>
672      an 1.230 PopUp.Evaluate		:Evaluate<CR>
673    endif
674  endif
675
676  let &cpo = save_cpo
677endfunc
678
679let s:winbar_winids = []
680
681" Install the window toolbar in the current window.
682func s:InstallWinbar()
683  if has('menu') && &mouse != ''
684    nnoremenu WinBar.Step   :Step<CR>
685    nnoremenu WinBar.Next   :Over<CR>
686    nnoremenu WinBar.Finish :Finish<CR>
687    nnoremenu WinBar.Cont   :Continue<CR>
688    nnoremenu WinBar.Stop   :Stop<CR>
689    nnoremenu WinBar.Eval   :Evaluate<CR>
690    call add(s:winbar_winids, win_getid(winnr()))
691  endif
692endfunc
693
694" Delete installed debugger commands in the current window.
695func s:DeleteCommands()
696  delcommand Break
697  delcommand Clear
698  delcommand Step
699  delcommand Over
700  delcommand Finish
701  delcommand Run
702  delcommand Arguments
703  delcommand Stop
704  delcommand Continue
705  delcommand Evaluate
706  delcommand Gdb
707  delcommand Program
708  delcommand Source
709  delcommand Winbar
710
711  nunmap K
712
713  if has('menu')
714    " Remove the WinBar entries from all windows where it was added.
715    let curwinid = win_getid(winnr())
716    for winid in s:winbar_winids
717      if win_gotoid(winid)
718	aunmenu WinBar.Step
719	aunmenu WinBar.Next
720	aunmenu WinBar.Finish
721	aunmenu WinBar.Cont
722	aunmenu WinBar.Stop
723	aunmenu WinBar.Eval
724      endif
725    endfor
726    call win_gotoid(curwinid)
727    let s:winbar_winids = []
728
729    if exists('s:saved_mousemodel')
730      let &mousemodel = s:saved_mousemodel
731      unlet s:saved_mousemodel
732      aunmenu PopUp.-SEP3-
733      aunmenu PopUp.Set\ breakpoint
734      aunmenu PopUp.Clear\ breakpoint
735      aunmenu PopUp.Evaluate
736    endif
737  endif
738
739  exe 'sign unplace ' . s:pc_id
740  for [id, entries] in items(s:breakpoints)
741    for subid in keys(entries)
742      exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
743    endfor
744  endfor
745  unlet s:breakpoints
746  unlet s:breakpoint_locations
747
748  sign undefine debugPC
749  for val in s:BreakpointSigns
750    exe "sign undefine debugBreakpoint" . val
751  endfor
752  let s:BreakpointSigns = []
753endfunc
754
755" :Break - Set a breakpoint at the cursor position.
756func s:SetBreakpoint(at)
757  " Setting a breakpoint may not work while the program is running.
758  " Interrupt to make it work.
759  let do_continue = 0
760  if !s:stopped
761    let do_continue = 1
762    if s:way == 'prompt'
763      call s:PromptInterrupt()
764    else
765      call s:SendCommand('-exec-interrupt')
766    endif
767    sleep 10m
768  endif
769
770  " Use the fname:lnum format, older gdb can't handle --source.
771  let at = empty(a:at) ?
772        \ fnameescape(expand('%:p')) . ':' . line('.') : a:at
773  call s:SendCommand('-break-insert ' . at)
774  if do_continue
775    call s:SendCommand('-exec-continue')
776  endif
777endfunc
778
779" :Clear - Delete a breakpoint at the cursor position.
780func s:ClearBreakpoint()
781  let fname = fnameescape(expand('%:p'))
782  let lnum = line('.')
783  let bploc = printf('%s:%d', fname, lnum)
784  if has_key(s:breakpoint_locations, bploc)
785    let idx = 0
786    for id in s:breakpoint_locations[bploc]
787      if has_key(s:breakpoints, id)
788	" Assume this always works, the reply is simply "^done".
789	call s:SendCommand('-break-delete ' . id)
790	for subid in keys(s:breakpoints[id])
791	  exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
792	endfor
793	unlet s:breakpoints[id]
794	unlet s:breakpoint_locations[bploc][idx]
795	break
796      else
797	let idx += 1
798      endif
799    endfor
800    if empty(s:breakpoint_locations[bploc])
801      unlet s:breakpoint_locations[bploc]
802    endif
803  endif
804endfunc
805
806func s:Run(args)
807  if a:args != ''
808    call s:SendCommand('-exec-arguments ' . a:args)
809  endif
810  call s:SendCommand('-exec-run')
811endfunc
812
813func s:SendEval(expr)
814  call s:SendCommand('-data-evaluate-expression "' . a:expr . '"')
815  let s:evalexpr = a:expr
816endfunc
817
818" :Evaluate - evaluate what is under the cursor
819func s:Evaluate(range, arg)
820  if a:arg != ''
821    let expr = a:arg
822  elseif a:range == 2
823    let pos = getcurpos()
824    let reg = getreg('v', 1, 1)
825    let regt = getregtype('v')
826    normal! gv"vy
827    let expr = @v
828    call setpos('.', pos)
829    call setreg('v', reg, regt)
830  else
831    let expr = expand('<cexpr>')
832  endif
833  let s:ignoreEvalError = 0
834  call s:SendEval(expr)
835endfunc
836
837let s:ignoreEvalError = 0
838let s:evalFromBalloonExpr = 0
839
840" Handle the result of data-evaluate-expression
841func s:HandleEvaluate(msg)
842  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
843  let value = substitute(value, '\\"', '"', 'g')
844  if s:evalFromBalloonExpr
845    if s:evalFromBalloonExprResult == ''
846      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
847    else
848      let s:evalFromBalloonExprResult .= ' = ' . value
849    endif
850    call balloon_show(s:evalFromBalloonExprResult)
851  else
852    echomsg '"' . s:evalexpr . '": ' . value
853  endif
854
855  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
856    " Looks like a pointer, also display what it points to.
857    let s:ignoreEvalError = 1
858    call s:SendEval('*' . s:evalexpr)
859  else
860    let s:evalFromBalloonExpr = 0
861  endif
862endfunc
863
864" Show a balloon with information of the variable under the mouse pointer,
865" if there is any.
866func TermDebugBalloonExpr()
867  if v:beval_winid != s:sourcewin
868    return ''
869  endif
870  if !s:stopped
871    " Only evaluate when stopped, otherwise setting a breakpoint using the
872    " mouse triggers a balloon.
873    return ''
874  endif
875  let s:evalFromBalloonExpr = 1
876  let s:evalFromBalloonExprResult = ''
877  let s:ignoreEvalError = 1
878  call s:SendEval(v:beval_text)
879  return ''
880endfunc
881
882" Handle an error.
883func s:HandleError(msg)
884  if s:ignoreEvalError
885    " Result of s:SendEval() failed, ignore.
886    let s:ignoreEvalError = 0
887    let s:evalFromBalloonExpr = 0
888    return
889  endif
890  echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
891endfunc
892
893func s:GotoSourcewinOrCreateIt()
894  if !win_gotoid(s:sourcewin)
895    new
896    let s:sourcewin = win_getid(winnr())
897    call s:InstallWinbar()
898  endif
899endfunc
900
901" Handle stopping and running message from gdb.
902" Will update the sign that shows the current position.
903func s:HandleCursor(msg)
904  let wid = win_getid(winnr())
905
906  if a:msg =~ '^\*stopped'
907    call ch_log('program stopped')
908    let s:stopped = 1
909  elseif a:msg =~ '^\*running'
910    call ch_log('program running')
911    let s:stopped = 0
912  endif
913
914  if a:msg =~ 'fullname='
915    let fname = s:GetFullname(a:msg)
916  else
917    let fname = ''
918  endif
919  if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
920    let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
921    if lnum =~ '^[0-9]*$'
922    call s:GotoSourcewinOrCreateIt()
923      if expand('%:p') != fnamemodify(fname, ':p')
924	if &modified
925	  " TODO: find existing window
926	  exe 'split ' . fnameescape(fname)
927	  let s:sourcewin = win_getid(winnr())
928	  call s:InstallWinbar()
929	else
930	  exe 'edit ' . fnameescape(fname)
931	endif
932      endif
933      exe lnum
934      exe 'sign unplace ' . s:pc_id
935      exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname
936      setlocal signcolumn=yes
937    endif
938  elseif !s:stopped || fname != ''
939    exe 'sign unplace ' . s:pc_id
940  endif
941
942  call win_gotoid(wid)
943endfunc
944
945let s:BreakpointSigns = []
946
947func s:CreateBreakpoint(id, subid)
948  let nr = printf('%d.%d', a:id, a:subid)
949  if index(s:BreakpointSigns, nr) == -1
950    call add(s:BreakpointSigns, nr)
951    exe "sign define debugBreakpoint" . nr . " text=" . substitute(nr, '\..*', '', '') . " texthl=debugBreakpoint"
952  endif
953endfunc
954
955func! s:SplitMsg(s)
956  return split(a:s, '{.\{-}}\zs')
957endfunction
958
959" Handle setting a breakpoint
960" Will update the sign that shows the breakpoint
961func s:HandleNewBreakpoint(msg)
962  if a:msg !~ 'fullname='
963    " a watch does not have a file name
964    return
965  endif
966  for msg in s:SplitMsg(a:msg)
967    let fname = s:GetFullname(msg)
968    if empty(fname)
969      continue
970    endif
971    let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '')
972    if empty(nr)
973      return
974    endif
975
976    " If "nr" is 123 it becomes "123.0" and subid is "0".
977    " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded.
978    let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0')
979    call s:CreateBreakpoint(id, subid)
980
981    if has_key(s:breakpoints, id)
982      let entries = s:breakpoints[id]
983    else
984      let entries = {}
985      let s:breakpoints[id] = entries
986    endif
987    if has_key(entries, subid)
988      let entry = entries[subid]
989    else
990      let entry = {}
991      let entries[subid] = entry
992    endif
993
994    let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '')
995    let entry['fname'] = fname
996    let entry['lnum'] = lnum
997
998    let bploc = printf('%s:%d', fname, lnum)
999    if !has_key(s:breakpoint_locations, bploc)
1000      let s:breakpoint_locations[bploc] = []
1001    endif
1002    let s:breakpoint_locations[bploc] += [id]
1003
1004    if bufloaded(fname)
1005      call s:PlaceSign(id, subid, entry)
1006    endif
1007  endfor
1008endfunc
1009
1010func s:PlaceSign(id, subid, entry)
1011  let nr = printf('%d.%d', a:id, a:subid)
1012  exe 'sign place ' . s:Breakpoint2SignNumber(a:id, a:subid) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . nr . ' file=' . a:entry['fname']
1013  let a:entry['placed'] = 1
1014endfunc
1015
1016" Handle deleting a breakpoint
1017" Will remove the sign that shows the breakpoint
1018func s:HandleBreakpointDelete(msg)
1019  let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
1020  if empty(id)
1021    return
1022  endif
1023  if has_key(s:breakpoints, id)
1024    for [subid, entry] in items(s:breakpoints[id])
1025      if has_key(entry, 'placed')
1026	exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
1027	unlet entry['placed']
1028      endif
1029    endfor
1030    unlet s:breakpoints[id]
1031  endif
1032endfunc
1033
1034" Handle the debugged program starting to run.
1035" Will store the process ID in s:pid
1036func s:HandleProgramRun(msg)
1037  let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0
1038  if nr == 0
1039    return
1040  endif
1041  let s:pid = nr
1042  call ch_log('Detected process ID: ' . s:pid)
1043endfunc
1044
1045" Handle a BufRead autocommand event: place any signs.
1046func s:BufRead()
1047  let fname = expand('<afile>:p')
1048  for [id, entries] in items(s:breakpoints)
1049    for [subid, entry] in items(entries)
1050      if entry['fname'] == fname
1051	call s:PlaceSign(id, subid, entry)
1052      endif
1053    endfor
1054  endfor
1055endfunc
1056
1057" Handle a BufUnloaded autocommand event: unplace any signs.
1058func s:BufUnloaded()
1059  let fname = expand('<afile>:p')
1060  for [id, entries] in items(s:breakpoints)
1061    for [subid, entry] in items(entries)
1062      if entry['fname'] == fname
1063	let entry['placed'] = 0
1064      endif
1065    endfor
1066  endfor
1067endfunc
1068
1069let &cpo = s:keepcpo
1070unlet s:keepcpo
1071