1" Debugger plugin using gdb.
2"
3" Author: Bram Moolenaar
4" Copyright: Vim license applies, see ":help license"
5" Last Change: 2021 Nov 27
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" For neovim compatibility, the vim specific calls were replaced with neovim
38" specific calls:
39"   term_start -> term_open
40"   term_sendkeys -> chansend
41"   term_getline -> getbufline
42"   job_info && term_getjob -> using linux command ps to get the tty
43"   balloon -> nvim floating window
44"
45" The code for opening the floating window was taken from the beautiful
46" implementation of LanguageClient-Neovim:
47" https://github.com/autozimu/LanguageClient-neovim/blob/0ed9b69dca49c415390a8317b19149f97ae093fa/autoload/LanguageClient.vim#L304
48"
49" Neovim terminal also works seamlessly on windows, which is why the ability
50" Author: Bram Moolenaar
51" Copyright: Vim license applies, see ":help license"
52
53" In case this gets sourced twice.
54if exists(':Termdebug')
55  finish
56endif
57
58" The terminal feature does not work with gdb on win32.
59if !has('win32')
60  let s:way = 'terminal'
61else
62  let s:way = 'prompt'
63endif
64
65let s:keepcpo = &cpo
66set cpo&vim
67
68" The command that starts debugging, e.g. ":Termdebug vim".
69" To end type "quit" in the gdb window.
70command -nargs=* -complete=file -bang Termdebug call s:StartDebug(<bang>0, <f-args>)
71command -nargs=+ -complete=file -bang TermdebugCommand call s:StartDebugCommand(<bang>0, <f-args>)
72
73" Name of the gdb command, defaults to "gdb".
74if !exists('g:termdebugger')
75  let g:termdebugger = 'gdb'
76endif
77
78let s:pc_id = 12
79let s:asm_id = 13
80let s:break_id = 14  " breakpoint number is added to this
81let s:stopped = 1
82
83let s:parsing_disasm_msg = 0
84let s:asm_lines = []
85let s:asm_addr = ''
86
87" Take a breakpoint number as used by GDB and turn it into an integer.
88" The breakpoint may contain a dot: 123.4 -> 123004
89" The main breakpoint has a zero subid.
90func s:Breakpoint2SignNumber(id, subid)
91  return s:break_id + a:id * 1000 + a:subid
92endfunction
93
94func s:Highlight(init, old, new)
95  let default = a:init ? 'default ' : ''
96  if a:new ==# 'light' && a:old !=# 'light'
97    exe "hi " . default . "debugPC term=reverse ctermbg=lightblue guibg=lightblue"
98  elseif a:new ==# 'dark' && a:old !=# 'dark'
99    exe "hi " . default . "debugPC term=reverse ctermbg=darkblue guibg=darkblue"
100  endif
101endfunc
102
103call s:Highlight(1, '', &background)
104hi default debugBreakpoint term=reverse ctermbg=red guibg=red
105hi default debugBreakpointDisabled term=reverse ctermbg=gray guibg=gray
106
107func s:StartDebug(bang, ...)
108  " First argument is the command to debug, second core file or process ID.
109  call s:StartDebug_internal({'gdb_args': a:000, 'bang': a:bang})
110endfunc
111
112func s:StartDebugCommand(bang, ...)
113  " First argument is the command to debug, rest are run arguments.
114  call s:StartDebug_internal({'gdb_args': [a:1], 'proc_args': a:000[1:], 'bang': a:bang})
115endfunc
116
117func s:StartDebug_internal(dict)
118  if exists('s:gdbwin')
119    echoerr 'Terminal debugger already running, cannot run two'
120    return
121  endif
122  if !executable(g:termdebugger)
123    echoerr 'Cannot execute debugger program "' .. g:termdebugger .. '"'
124    return
125  endif
126
127  let s:ptywin = 0
128  let s:pid = 0
129  let s:asmwin = 0
130
131  if exists('#User#TermdebugStartPre')
132    doauto <nomodeline> User TermdebugStartPre
133  endif
134
135  " Uncomment this line to write logging in "debuglog".
136  " call ch_logfile('debuglog', 'w')
137
138  let s:sourcewin = win_getid(winnr())
139
140  " Remember the old value of 'signcolumn' for each buffer that it's set in, so
141  " that we can restore the value for all buffers.
142  let b:save_signcolumn = &signcolumn
143  let s:signcolumn_buflist = [bufnr()]
144
145  let s:save_columns = 0
146  let s:allleft = 0
147  if exists('g:termdebug_wide')
148    if &columns < g:termdebug_wide
149      let s:save_columns = &columns
150      let &columns = g:termdebug_wide
151      " If we make the Vim window wider, use the whole left halve for the debug
152      " windows.
153      let s:allleft = 1
154    endif
155    let s:vertical = 1
156  else
157    let s:vertical = 0
158  endif
159
160  " Override using a terminal window by setting g:termdebug_use_prompt to 1.
161  let use_prompt = exists('g:termdebug_use_prompt') && g:termdebug_use_prompt
162  if !has('win32') && !use_prompt
163    let s:way = 'terminal'
164   else
165    let s:way = 'prompt'
166   endif
167
168  if s:way == 'prompt'
169    call s:StartDebug_prompt(a:dict)
170  else
171    call s:StartDebug_term(a:dict)
172  endif
173
174  if exists('g:termdebug_disasm_window')
175    if g:termdebug_disasm_window
176      let curwinid = win_getid(winnr())
177      call s:GotoAsmwinOrCreateIt()
178      call win_gotoid(curwinid)
179    endif
180  endif
181
182  if exists('#User#TermdebugStartPost')
183    doauto <nomodeline> User TermdebugStartPost
184  endif
185endfunc
186
187" Use when debugger didn't start or ended.
188func s:CloseBuffers()
189  exe 'bwipe! ' . s:ptybuf
190  unlet! s:gdbwin
191endfunc
192
193func s:CheckGdbRunning()
194  if nvim_get_chan_info(s:gdb_job_id) == {}
195      echoerr string(g:termdebugger) . ' exited unexpectedly'
196      call s:CloseBuffers()
197      return ''
198  endif
199  return 'ok'
200endfunc
201
202func s:StartDebug_term(dict)
203  " Open a terminal window without a job, to run the debugged program in.
204  execute s:vertical ? 'vnew' : 'new'
205  let s:pty_job_id = termopen('tail -f /dev/null;#gdb program')
206  if s:pty_job_id == 0
207    echoerr 'invalid argument (or job table is full) while opening terminal window'
208    return
209  elseif s:pty_job_id == -1
210    echoerr 'Failed to open the program terminal window'
211    return
212  endif
213  let pty_job_info = nvim_get_chan_info(s:pty_job_id)
214  let s:ptybuf = pty_job_info['buffer']
215  let pty = pty_job_info['pty']
216  let s:ptywin = win_getid(winnr())
217  if s:vertical
218    " Assuming the source code window will get a signcolumn, use two more
219    " columns for that, thus one less for the terminal window.
220    exe (&columns / 2 - 1) . "wincmd |"
221    if s:allleft
222      " use the whole left column
223      wincmd H
224    endif
225  endif
226
227  " Create a hidden terminal window to communicate with gdb
228  let s:comm_job_id = jobstart('tail -f /dev/null;#gdb communication', {
229        \ 'on_stdout': function('s:CommOutput'),
230        \ 'pty': v:true,
231        \ })
232  " hide terminal buffer
233  if s:comm_job_id == 0
234    echoerr 'invalid argument (or job table is full) while opening communication terminal window'
235    exe 'bwipe! ' . s:ptybuf
236    return
237  elseif s:comm_job_id == -1
238    echoerr 'Failed to open the communication terminal window'
239    exe 'bwipe! ' . s:ptybuf
240    return
241  endif
242  let comm_job_info = nvim_get_chan_info(s:comm_job_id)
243  let commpty = comm_job_info['pty']
244
245  let gdb_args = get(a:dict, 'gdb_args', [])
246  let proc_args = get(a:dict, 'proc_args', [])
247
248  let gdb_cmd = [g:termdebugger]
249  " Add -quiet to avoid the intro message causing a hit-enter prompt.
250  let gdb_cmd += ['-quiet']
251  " Disable pagination, it causes everything to stop at the gdb
252  let gdb_cmd += ['-iex', 'set pagination off']
253  " Interpret commands while the target is running.  This should usually only
254  " be exec-interrupt, since many commands don't work properly while the
255  " target is running (so execute during startup).
256  let gdb_cmd += ['-iex', 'set mi-async on']
257  " Open a terminal window to run the debugger.
258  let gdb_cmd += ['-tty', pty]
259  " Command executed _after_ startup is done, provides us with the necessary feedback
260  let gdb_cmd += ['-ex', 'echo startupdone\n']
261
262  " Adding arguments requested by the user
263  let gdb_cmd += gdb_args
264
265  execute 'new'
266  " call ch_log('executing "' . join(gdb_cmd) . '"')
267  let s:gdb_job_id = termopen(gdb_cmd, {'on_exit': function('s:EndTermDebug')})
268  if s:gdb_job_id == 0
269    echoerr 'invalid argument (or job table is full) while opening gdb terminal window'
270    exe 'bwipe! ' . s:ptybuf
271    return
272  elseif s:gdb_job_id == -1
273    echoerr 'Failed to open the gdb terminal window'
274    call s:CloseBuffers()
275    return
276  endif
277  let gdb_job_info = nvim_get_chan_info(s:gdb_job_id)
278  let s:gdbbuf = gdb_job_info['buffer']
279  let s:gdbwin = win_getid(winnr())
280
281  " Wait for the "startupdone" message before sending any commands.
282  let try_count = 0
283  while 1
284    if s:CheckGdbRunning() != 'ok'
285      return
286    endif
287
288    for lnum in range(1, 200)
289      if get(getbufline(s:gdbbuf, lnum), 0, '') =~ 'startupdone'
290        let try_count = 9999
291        break
292      endif
293    endfor
294    let try_count += 1
295    if try_count > 300
296      " done or give up after five seconds
297      break
298    endif
299    sleep 10m
300  endwhile
301
302  " Set arguments to be run.
303  if len(proc_args)
304    call chansend(s:gdb_job_id, 'server set args ' . join(proc_args) . "\r")
305  endif
306
307  " Connect gdb to the communication pty, using the GDB/MI interface.
308  " Prefix "server" to avoid adding this to the history.
309  call chansend(s:gdb_job_id, 'server new-ui mi ' . commpty . "\r")
310
311  " Wait for the response to show up, users may not notice the error and wonder
312  " why the debugger doesn't work.
313  let try_count = 0
314  while 1
315    if s:CheckGdbRunning() != 'ok'
316      return
317    endif
318
319    let response = ''
320    for lnum in range(1, 200)
321      let line1 = get(getbufline(s:gdbbuf, lnum), 0, '')
322      let line2 = get(getbufline(s:gdbbuf, lnum + 1), 0, '')
323      if line1 =~ 'new-ui mi '
324        " response can be in the same line or the next line
325        let response = line1 . line2
326        if response =~ 'Undefined command'
327          echoerr 'Sorry, your gdb is too old, gdb 7.12 is required'
328          " CHECKME: possibly send a "server show version" here
329          call s:CloseBuffers()
330          return
331        endif
332        if response =~ 'New UI allocated'
333          " Success!
334          break
335        endif
336      elseif line1 =~ 'Reading symbols from' && line2 !~ 'new-ui mi '
337        " Reading symbols might take a while, try more times
338        let try_count -= 1
339      endif
340    endfor
341    if response =~ 'New UI allocated'
342      break
343    endif
344    let try_count += 1
345    if try_count > 100
346      echoerr 'Cannot check if your gdb works, continuing anyway'
347      break
348    endif
349    sleep 10m
350  endwhile
351
352  " Set the filetype, this can be used to add mappings.
353  set filetype=termdebug
354
355  call s:StartDebugCommon(a:dict)
356endfunc
357
358func s:StartDebug_prompt(dict)
359  " Open a window with a prompt buffer to run gdb in.
360  if s:vertical
361    vertical new
362  else
363    new
364  endif
365  let s:gdbwin = win_getid(winnr())
366  let s:promptbuf = bufnr('')
367  call prompt_setprompt(s:promptbuf, 'gdb> ')
368  set buftype=prompt
369  file gdb
370  call prompt_setcallback(s:promptbuf, function('s:PromptCallback'))
371  call prompt_setinterrupt(s:promptbuf, function('s:PromptInterrupt'))
372
373  if s:vertical
374    " Assuming the source code window will get a signcolumn, use two more
375    " columns for that, thus one less for the terminal window.
376    exe (&columns / 2 - 1) . "wincmd |"
377  endif
378
379  let gdb_args = get(a:dict, 'gdb_args', [])
380  let proc_args = get(a:dict, 'proc_args', [])
381
382  let gdb_cmd = [g:termdebugger]
383  " Add -quiet to avoid the intro message causing a hit-enter prompt.
384  let gdb_cmd += ['-quiet']
385  " Disable pagination, it causes everything to stop at the gdb, needs to be run early
386  let gdb_cmd += ['-iex', 'set pagination off']
387  " Interpret commands while the target is running.  This should usually only
388  " be exec-interrupt, since many commands don't work properly while the
389  " target is running (so execute during startup).
390  let gdb_cmd += ['-iex', 'set mi-async on']
391  " directly communicate via mi2
392  let gdb_cmd += ['--interpreter=mi2']
393
394  " Adding arguments requested by the user
395  let gdb_cmd += gdb_args
396
397  " call ch_log('executing "' . join(gdb_cmd) . '"')
398  let s:gdbjob = jobstart(gdb_cmd, {
399    \ 'on_exit': function('s:EndPromptDebug'),
400    \ 'on_stdout': function('s:GdbOutCallback'),
401    \ })
402  if s:gdbjob == 0
403    echoerr 'invalid argument (or job table is full) while starting gdb job'
404    exe 'bwipe! ' . s:ptybuf
405    return
406  elseif s:gdbjob == -1
407    echoerr 'Failed to start the gdb job'
408    call s:CloseBuffers()
409    return
410  endif
411
412  let s:ptybuf = 0
413  if has('win32')
414    " MS-Windows: run in a new console window for maximum compatibility
415    call s:SendCommand('set new-console on')
416  else
417    " Unix: Run the debugged program in a terminal window.  Open it below the
418    " gdb window.
419    execute 'new'
420    wincmd x | wincmd j
421    belowright let s:pty_job_id = termopen('tail -f /dev/null;#gdb program')
422    if s:pty_job_id == 0
423      echoerr 'invalid argument (or job table is full) while opening terminal window'
424      return
425    elseif s:pty_job_id == -1
426      echoerr 'Failed to open the program terminal window'
427      return
428    endif
429    let pty_job_info = nvim_get_chan_info(s:pty_job_id)
430    let s:ptybuf = pty_job_info['buffer']
431    let pty = pty_job_info['pty']
432    let s:ptywin = win_getid(winnr())
433    call s:SendCommand('tty ' . pty)
434
435    " Since GDB runs in a prompt window, the environment has not been set to
436    " match a terminal window, need to do that now.
437    call s:SendCommand('set env TERM = xterm-color')
438    call s:SendCommand('set env ROWS = ' . winheight(s:ptywin))
439    call s:SendCommand('set env LINES = ' . winheight(s:ptywin))
440    call s:SendCommand('set env COLUMNS = ' . winwidth(s:ptywin))
441    call s:SendCommand('set env COLORS = ' . &t_Co)
442    call s:SendCommand('set env VIM_TERMINAL = ' . v:version)
443  endif
444  call s:SendCommand('set print pretty on')
445  call s:SendCommand('set breakpoint pending on')
446
447  " Set arguments to be run
448  if len(proc_args)
449    call s:SendCommand('set args ' . join(proc_args))
450  endif
451
452  call s:StartDebugCommon(a:dict)
453  startinsert
454endfunc
455
456func s:StartDebugCommon(dict)
457  " Sign used to highlight the line where the program has stopped.
458  " There can be only one.
459  sign define debugPC linehl=debugPC
460
461  " Install debugger commands in the text window.
462  call win_gotoid(s:sourcewin)
463  call s:InstallCommands()
464  call win_gotoid(s:gdbwin)
465
466  " Contains breakpoints that have been placed, key is a string with the GDB
467  " breakpoint number.
468  " Each entry is a dict, containing the sub-breakpoints.  Key is the subid.
469  " For a breakpoint that is just a number the subid is zero.
470  " For a breakpoint "123.4" the id is "123" and subid is "4".
471  " Example, when breakpoint "44", "123", "123.1" and "123.2" exist:
472  " {'44': {'0': entry}, '123': {'0': entry, '1': entry, '2': entry}}
473  let s:breakpoints = {}
474
475  " Contains breakpoints by file/lnum.  The key is "fname:lnum".
476  " Each entry is a list of breakpoint IDs at that position.
477  let s:breakpoint_locations = {}
478
479  augroup TermDebug
480    au BufRead * call s:BufRead()
481    au BufUnload * call s:BufUnloaded()
482    au OptionSet background call s:Highlight(0, v:option_old, v:option_new)
483  augroup END
484
485  " Run the command if the bang attribute was given and got to the debug
486  " window.
487  if get(a:dict, 'bang', 0)
488    call s:SendCommand('-exec-run')
489    call win_gotoid(s:ptywin)
490  endif
491endfunc
492
493" Send a command to gdb.  "cmd" is the string without line terminator.
494func s:SendCommand(cmd)
495  "call ch_log('sending to gdb: ' . a:cmd)
496  if s:way == 'prompt'
497    call chansend(s:gdbjob, a:cmd . "\n")
498  else
499    call chansend(s:comm_job_id, a:cmd . "\r")
500  endif
501endfunc
502
503" This is global so that a user can create their mappings with this.
504func TermDebugSendCommand(cmd)
505  if s:way == 'prompt'
506    call chansend(s:gdbjob, a:cmd . "\n")
507  else
508    let do_continue = 0
509    if !s:stopped
510      let do_continue = 1
511      if s:way == 'prompt'
512        " Need to send a signal to get the UI to listen.  Strangely this is only
513        " needed once.
514        call jobstop(s:gdbjob)
515      else
516        Stop
517      endif
518      sleep 10m
519    endif
520    call chansend(s:gdb_job_id, a:cmd . "\r")
521    if do_continue
522      Continue
523    endif
524  endif
525endfunc
526
527" Send a command only when stopped.  Used for :Next and :Step.
528func s:SendCommandIfStopped(cmd)
529  if s:stopped
530    call s:SendCommand(a:cmd)
531  " else
532    " call ch_log('dropping command, program is running: ' . a:cmd)
533  endif
534endfunc
535
536" Function called when entering a line in the prompt buffer.
537func s:PromptCallback(text)
538  call s:SendCommand(a:text)
539endfunc
540
541" Function called when pressing CTRL-C in the prompt buffer and when placing a
542" breakpoint.
543func s:PromptInterrupt()
544  " call ch_log('Interrupting gdb')
545  if has('win32')
546    " Using job_stop() does not work on MS-Windows, need to send SIGTRAP to
547    " the debugger program so that gdb responds again.
548    if s:pid == 0
549      echoerr 'Cannot interrupt gdb, did not find a process ID'
550    else
551      call debugbreak(s:pid)
552    endif
553  else
554    call jobstop(s:gdbjob)
555  endif
556endfunc
557
558" Function called when gdb outputs text.
559func s:GdbOutCallback(job_id, msgs, event)
560  "call ch_log('received from gdb: ' . a:text)
561
562  " Drop the gdb prompt, we have our own.
563  " Drop status and echo'd commands.
564  call filter(a:msgs, { index, val ->
565        \ val !=# '(gdb)' && val !=# '^done' && val[0] !=# '&'})
566
567  let lines = []
568  let index = 0
569
570  for msg in a:msgs
571    if msg =~ '^\^error,msg='
572      if exists('s:evalexpr')
573            \ && s:DecodeMessage(msg[11:])
574            \    =~ 'A syntax error in expression, near\|No symbol .* in current context'
575        " Silently drop evaluation errors.
576        call remove(a:msgs, index)
577        unlet s:evalexpr
578        continue
579      endif
580    elseif msg[0] == '~'
581      call add(lines, s:DecodeMessage(msg[1:]))
582      call remove(a:msgs, index)
583      continue
584    endif
585    let index += 1
586  endfor
587
588  let curwinid = win_getid(winnr())
589  call win_gotoid(s:gdbwin)
590
591  " Add the output above the current prompt.
592  for line in lines
593    call append(line('$') - 1, line)
594  endfor
595  if !empty(lines)
596    set modified
597  endif
598
599  call win_gotoid(curwinid)
600  call s:CommOutput(a:job_id, a:msgs, a:event)
601endfunc
602
603" Decode a message from gdb.  quotedText starts with a ", return the text up
604" to the next ", unescaping characters:
605" - remove line breaks
606" - change \\t to \t
607" - change \0xhh to \xhh
608" - change \ooo to octal
609" - change \\ to \
610func s:DecodeMessage(quotedText)
611  if a:quotedText[0] != '"'
612    echoerr 'DecodeMessage(): missing quote in ' . a:quotedText
613    return
614  endif
615  return a:quotedText
616        \->substitute('^"\|".*\|\\n', '', 'g')
617        \->substitute('\\t', "\t", 'g')
618        \->substitute('\\0x\(\x\x\)', {-> eval('"\x' .. submatch(1) .. '"')}, 'g')
619        \->substitute('\\\o\o\o', {-> eval('"' .. submatch(0) .. '"')}, 'g')
620        \->substitute('\\\\', '\', 'g')
621endfunc
622
623" Extract the "name" value from a gdb message with fullname="name".
624func s:GetFullname(msg)
625  if a:msg !~ 'fullname'
626    return ''
627  endif
628  let name = s:DecodeMessage(substitute(a:msg, '.*fullname=', '', ''))
629  if has('win32') && name =~ ':\\\\'
630    " sometimes the name arrives double-escaped
631    let name = substitute(name, '\\\\', '\\', 'g')
632  endif
633  return name
634endfunc
635
636" Extract the "addr" value from a gdb message with addr="0x0001234".
637func s:GetAsmAddr(msg)
638  if a:msg !~ 'addr='
639    return ''
640  endif
641  let addr = s:DecodeMessage(substitute(a:msg, '.*addr=', '', ''))
642  return addr
643endfunc
644
645function s:EndTermDebug(job_id, exit_code, event)
646  if exists('#User#TermdebugStopPre')
647    doauto <nomodeline> User TermdebugStopPre
648  endif
649
650  unlet s:gdbwin
651
652  call s:EndDebugCommon()
653endfunc
654
655func s:EndDebugCommon()
656  let curwinid = win_getid(winnr())
657
658  if exists('s:ptybuf') && s:ptybuf
659    exe 'bwipe! ' . s:ptybuf
660  endif
661
662  " Restore 'signcolumn' in all buffers for which it was set.
663  call win_gotoid(s:sourcewin)
664  let was_buf = bufnr()
665  for bufnr in s:signcolumn_buflist
666    if bufexists(bufnr)
667      exe bufnr .. "buf"
668      if exists('b:save_signcolumn')
669        let &signcolumn = b:save_signcolumn
670        unlet b:save_signcolumn
671      endif
672    endif
673  endfor
674  exe was_buf .. "buf"
675
676  call s:DeleteCommands()
677
678  call win_gotoid(curwinid)
679
680  if s:save_columns > 0
681    let &columns = s:save_columns
682  endif
683
684  if exists('#User#TermdebugStopPost')
685    doauto <nomodeline> User TermdebugStopPost
686  endif
687
688  au! TermDebug
689endfunc
690
691func s:EndPromptDebug(job_id, exit_code, event)
692  if exists('#User#TermdebugStopPre')
693    doauto <nomodeline> User TermdebugStopPre
694  endif
695
696  let curwinid = win_getid(winnr())
697  call win_gotoid(s:gdbwin)
698  close
699  if curwinid != s:gdbwin
700    call win_gotoid(curwinid)
701  endif
702
703  call s:EndDebugCommon()
704  unlet s:gdbwin
705  "call ch_log("Returning from EndPromptDebug()")
706endfunc
707
708" - CommOutput: disassemble $pc
709" - CommOutput: &"disassemble $pc\n"
710" - CommOutput: ~"Dump of assembler code for function main(int, char**):\n"
711" - CommOutput: ~"   0x0000555556466f69 <+0>:\tpush   rbp\n"
712" ...
713" - CommOutput: ~"   0x0000555556467cd0:\tpop    rbp\n"
714" - CommOutput: ~"   0x0000555556467cd1:\tret    \n"
715" - CommOutput: ~"End of assembler dump.\n"
716" - CommOutput: ^done
717
718" - CommOutput: disassemble $pc
719" - CommOutput: &"disassemble $pc\n"
720" - CommOutput: &"No function contains specified address.\n"
721" - CommOutput: ^error,msg="No function contains specified address."
722func s:HandleDisasmMsg(msg)
723  if a:msg =~ '^\^done'
724    let curwinid = win_getid(winnr())
725    if win_gotoid(s:asmwin)
726      silent normal! gg0"_dG
727      call setline(1, s:asm_lines)
728      set nomodified
729      set filetype=asm
730
731      let lnum = search('^' . s:asm_addr)
732      if lnum != 0
733        exe 'sign unplace ' . s:asm_id
734        exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC'
735      endif
736
737      call win_gotoid(curwinid)
738    endif
739
740    let s:parsing_disasm_msg = 0
741    let s:asm_lines = []
742  elseif a:msg =~ '^\^error,msg='
743    if s:parsing_disasm_msg == 1
744      " Disassemble call ran into an error. This can happen when gdb can't
745      " find the function frame address, so let's try to disassemble starting
746      " at current PC
747      call s:SendCommand('disassemble $pc,+100')
748    endif
749    let s:parsing_disasm_msg = 0
750  elseif a:msg =~ '\&\"disassemble \$pc'
751    if a:msg =~ '+100'
752      " This is our second disasm attempt
753      let s:parsing_disasm_msg = 2
754    endif
755  else
756    let value = substitute(a:msg, '^\~\"[ ]*', '', '')
757    let value = substitute(value, '^=>[ ]*', '', '')
758    let value = substitute(value, '\\n\"\r$', '', '')
759    let value = substitute(value, '\r', '', '')
760    let value = substitute(value, '\\t', ' ', 'g')
761
762    if value != '' || !empty(s:asm_lines)
763      call add(s:asm_lines, value)
764    endif
765  endif
766endfunc
767
768func s:CommOutput(job_id, msgs, event)
769
770  for msg in a:msgs
771    " remove prefixed NL
772    if msg[0] == "\n"
773      let msg = msg[1:]
774    endif
775
776    if s:parsing_disasm_msg
777      call s:HandleDisasmMsg(msg)
778    elseif msg != ''
779      if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)'
780        call s:HandleCursor(msg)
781      elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,'
782        call s:HandleNewBreakpoint(msg, 0)
783      elseif msg =~ '^=breakpoint-modified,'
784        call s:HandleNewBreakpoint(msg, 1)
785      elseif msg =~ '^=breakpoint-deleted,'
786        call s:HandleBreakpointDelete(msg)
787      elseif msg =~ '^=thread-group-started'
788        call s:HandleProgramRun(msg)
789      elseif msg =~ '^\^done,value='
790        call s:HandleEvaluate(msg)
791      elseif msg =~ '^\^error,msg='
792        call s:HandleError(msg)
793      elseif msg =~ '^disassemble'
794        let s:parsing_disasm_msg = 1
795        let s:asm_lines = []
796      endif
797    endif
798  endfor
799endfunc
800
801func s:GotoProgram()
802  if has('win32')
803    if executable('powershell')
804      call system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', s:pid))
805    endif
806  else
807    call win_gotoid(s:ptywin)
808  endif
809endfunc
810
811" Install commands in the current window to control the debugger.
812func s:InstallCommands()
813  let save_cpo = &cpo
814  set cpo&vim
815
816  command -nargs=? Break call s:SetBreakpoint(<q-args>)
817  command Clear call s:ClearBreakpoint()
818  command Step call s:SendCommandIfStopped('-exec-step')
819  command Over call s:SendCommandIfStopped('-exec-next')
820  command Finish call s:SendCommandIfStopped('-exec-finish')
821  command -nargs=* Run call s:Run(<q-args>)
822  command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>)
823
824  if s:way == 'prompt'
825    command Stop call s:PromptInterrupt()
826    command Continue call s:SendCommand('continue')
827  else
828    command Stop call s:SendCommand('-exec-interrupt')
829    " using -exec-continue results in CTRL-C in the gdb window not working,
830    " communicating via commbuf (= use of SendCommand) has the same result
831    "command Continue  call s:SendCommand('-exec-continue')
832    command Continue call chansend(s:gdb_job_id, "continue\r")
833  endif
834
835  command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
836  command Gdb call win_gotoid(s:gdbwin)
837  command Program call s:GotoProgram()
838  command Source call s:GotoSourcewinOrCreateIt()
839  command Asm call s:GotoAsmwinOrCreateIt()
840  command Winbar call s:InstallWinbar()
841
842  if !exists('g:termdebug_map_K') || g:termdebug_map_K
843    " let s:k_map_saved = maparg('K', 'n', 0, 1)
844    let s:k_map_saved = {}
845    for map in nvim_get_keymap('n')
846      if map.lhs ==# 'K'
847        let s:k_map_saved = map
848        break
849      endif
850    endfor
851    nnoremap K :Evaluate<CR>
852  endif
853
854  let &cpo = save_cpo
855endfunc
856
857" let s:winbar_winids = []
858
859" Install the window toolbar in the current window.
860func s:InstallWinbar()
861  " if has('menu') && &mouse != ''
862  "   nnoremenu WinBar.Step   :Step<CR>
863  "   nnoremenu WinBar.Next   :Over<CR>
864  "   nnoremenu WinBar.Finish :Finish<CR>
865  "   nnoremenu WinBar.Cont   :Continue<CR>
866  "   nnoremenu WinBar.Stop   :Stop<CR>
867  "   nnoremenu WinBar.Eval   :Evaluate<CR>
868  "   call add(s:winbar_winids, win_getid(winnr()))
869  " endif
870endfunc
871
872" Delete installed debugger commands in the current window.
873func s:DeleteCommands()
874  delcommand Break
875  delcommand Clear
876  delcommand Step
877  delcommand Over
878  delcommand Finish
879  delcommand Run
880  delcommand Arguments
881  delcommand Stop
882  delcommand Continue
883  delcommand Evaluate
884  delcommand Gdb
885  delcommand Program
886  delcommand Source
887  delcommand Asm
888  delcommand Winbar
889
890  if exists('s:k_map_saved')
891    if empty(s:k_map_saved)
892      nunmap K
893    else
894      " call mapset('n', 0, s:k_map_saved)
895      let mode = s:k_map_saved.mode !=# ' ' ? s:k_map_saved.mode : ''
896      call nvim_set_keymap(mode, 'K', s:k_map_saved.rhs, {
897        \ 'expr': s:k_map_saved.expr ? v:true : v:false,
898        \ 'noremap': s:k_map_saved.noremap ? v:true : v:false,
899        \ 'nowait': s:k_map_saved.nowait ? v:true : v:false,
900        \ 'script': s:k_map_saved.script ? v:true : v:false,
901        \ 'silent': s:k_map_saved.silent ? v:true : v:false,
902        \ })
903    endif
904    unlet s:k_map_saved
905  endif
906
907  exe 'sign unplace ' . s:pc_id
908  for [id, entries] in items(s:breakpoints)
909    for subid in keys(entries)
910      exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
911    endfor
912  endfor
913  unlet s:breakpoints
914  unlet s:breakpoint_locations
915
916  sign undefine debugPC
917  for val in s:BreakpointSigns
918    exe "sign undefine debugBreakpoint" . val
919  endfor
920  let s:BreakpointSigns = []
921endfunc
922
923" :Break - Set a breakpoint at the cursor position.
924func s:SetBreakpoint(at)
925  " Setting a breakpoint may not work while the program is running.
926  " Interrupt to make it work.
927  let do_continue = 0
928  if !s:stopped
929    let do_continue = 1
930    Stop
931    sleep 10m
932  endif
933
934  " Use the fname:lnum format, older gdb can't handle --source.
935  let at = empty(a:at) ?
936  \ fnameescape(expand('%:p')) . ':' . line('.') : a:at
937  call s:SendCommand('-break-insert ' . at)
938  if do_continue
939    Continue
940  endif
941endfunc
942
943" :Clear - Delete a breakpoint at the cursor position.
944func s:ClearBreakpoint()
945  let fname = fnameescape(expand('%:p'))
946  let lnum = line('.')
947  let bploc = printf('%s:%d', fname, lnum)
948  if has_key(s:breakpoint_locations, bploc)
949    let idx = 0
950    let nr = 0
951    for id in s:breakpoint_locations[bploc]
952      if has_key(s:breakpoints, id)
953        " Assume this always works, the reply is simply "^done".
954        call s:SendCommand('-break-delete ' . id)
955        for subid in keys(s:breakpoints[id])
956          exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
957        endfor
958        unlet s:breakpoints[id]
959        unlet s:breakpoint_locations[bploc][idx]
960        let nr = id
961        break
962      else
963        let idx += 1
964      endif
965    endfor
966    if nr != 0
967      if empty(s:breakpoint_locations[bploc])
968        unlet s:breakpoint_locations[bploc]
969      endif
970      echomsg 'Breakpoint ' . id . ' cleared from line ' . lnum . '.'
971    else
972      echoerr 'Internal error trying to remove breakpoint at line ' . lnum . '!'
973    endif
974  else
975    echomsg 'No breakpoint to remove at line ' . lnum . '.'
976  endif
977endfunc
978
979func s:Run(args)
980  if a:args != ''
981    call s:SendCommand('-exec-arguments ' . a:args)
982  endif
983  call s:SendCommand('-exec-run')
984endfunc
985
986func s:SendEval(expr)
987  " check for "likely" boolean expressions, in which case we take it as lhs
988  if a:expr =~ "[=!<>]="
989    let exprLHS = a:expr
990  else
991    " remove text that is likely an assignment
992    let exprLHS = substitute(a:expr, ' *=.*', '', '')
993  endif
994
995  " encoding expression to prevent bad errors
996  let expr = a:expr
997  let expr = substitute(expr, '\\', '\\\\', 'g')
998  let expr = substitute(expr, '"', '\\"', 'g')
999  call s:SendCommand('-data-evaluate-expression "' . expr . '"')
1000  let s:evalexpr = exprLHS
1001endfunc
1002
1003" :Evaluate - evaluate what is specified / under the cursor
1004func s:Evaluate(range, arg)
1005  let expr = s:GetEvaluationExpression(a:range, a:arg)
1006  let s:ignoreEvalError = 0
1007  call s:SendEval(expr)
1008endfunc
1009
1010" get what is specified / under the cursor
1011func s:GetEvaluationExpression(range, arg)
1012  if a:arg != ''
1013    " user supplied evaluation
1014    let expr = s:CleanupExpr(a:arg)
1015    " DSW: replace "likely copy + paste" assignment
1016    let expr = substitute(expr, '"\([^"]*\)": *', '\1=', 'g')
1017  elseif a:range == 2
1018    let pos = getcurpos()
1019    let reg = getreg('v', 1, 1)
1020    let regt = getregtype('v')
1021    normal! gv"vy
1022    let expr = s:CleanupExpr(@v)
1023    call setpos('.', pos)
1024    call setreg('v', reg, regt)
1025    let s:evalFromBalloonExpr = 1
1026  else
1027    " no evaluation provided: get from C-expression under cursor
1028    " TODO: allow filetype specific lookup #9057
1029    let expr = expand('<cexpr>')
1030    let s:evalFromBalloonExpr = 1
1031  endif
1032  return expr
1033endfunc
1034
1035" clean up expression that may got in because of range
1036" (newlines and surrounding whitespace)
1037" As it can also be specified via ex-command for assignments this function
1038" may not change the "content" parts (like replacing contained spaces
1039func s:CleanupExpr(expr)
1040  " replace all embedded newlines/tabs/...
1041  let expr = substitute(a:expr, '\_s', ' ', 'g')
1042
1043  if &filetype ==# 'cobol'
1044    " extra cleanup for COBOL:
1045    " - a semicolon nmay be used instead of a space
1046    " - a trailing comma or period is ignored as it commonly separates/ends
1047    "   multiple expr
1048    let expr = substitute(expr, ';', ' ', 'g')
1049    let expr = substitute(expr, '[,.]\+ *$', '', '')
1050  endif
1051
1052  " get rid of leading and trailing spaces
1053  let expr = substitute(expr, '^ *', '', '')
1054  let expr = substitute(expr, ' *$', '', '')
1055  return expr
1056endfunc
1057
1058let s:ignoreEvalError = 0
1059let s:evalFromBalloonExpr = 0
1060let s:evalFromBalloonExprResult = ''
1061
1062" Handle the result of data-evaluate-expression
1063func s:HandleEvaluate(msg)
1064  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
1065  let value = substitute(value, '\\"', '"', 'g')
1066  " multi-byte characters arrive in octal form
1067  let value = substitute(value, '\\\o\o\o', {-> eval('"' .. submatch(0) .. '"')}, 'g')
1068  let value = substitute(value, '
1069', '\1', '')
1070  if s:evalFromBalloonExpr
1071    if s:evalFromBalloonExprResult == ''
1072      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
1073    else
1074      let s:evalFromBalloonExprResult .= ' = ' . value
1075    endif
1076    let s:evalFromBalloonExprResult = split(s:evalFromBalloonExprResult, '\\n')
1077    call s:OpenHoverPreview(s:evalFromBalloonExprResult, v:null)
1078    let s:evalFromBalloonExprResult = ''
1079  else
1080    echomsg '"' . s:evalexpr . '": ' . value
1081  endif
1082
1083  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
1084    " Looks like a pointer, also display what it points to.
1085    let s:ignoreEvalError = 1
1086    call s:SendEval('*' . s:evalexpr)
1087  else
1088    let s:evalFromBalloonExprResult = ''
1089  endif
1090endfunc
1091
1092function! s:ShouldUseFloatWindow() abort
1093  if exists('*nvim_open_win') && (get(g:, 'termdebug_useFloatingHover', 1) == 1)
1094    return v:true
1095  else
1096    return v:false
1097  endif
1098endfunction
1099
1100function! s:CloseFloatingHoverOnCursorMove(win_id, opened) abort
1101  if getpos('.') == a:opened
1102    " Just after opening floating window, CursorMoved event is run.
1103    " To avoid closing floating window immediately, check the cursor
1104    " was really moved
1105    return
1106  endif
1107  autocmd! nvim_termdebug_close_hover
1108  let winnr = win_id2win(a:win_id)
1109  if winnr == 0
1110    return
1111  endif
1112  call nvim_win_close(a:win_id, v:true)
1113endfunction
1114
1115function! s:CloseFloatingHoverOnBufEnter(win_id, bufnr) abort
1116    let winnr = win_id2win(a:win_id)
1117    if winnr == 0
1118        " Float window was already closed
1119        autocmd! nvim_termdebug_close_hover
1120        return
1121    endif
1122    if winnr == winnr()
1123        " Cursor is moving into floating window. Do not close it
1124        return
1125    endif
1126    if bufnr('%') == a:bufnr
1127        " When current buffer opened hover window, it's not another buffer. Skipped
1128        return
1129    endif
1130    autocmd! nvim_termdebug_close_hover
1131    call nvim_win_close(a:win_id, v:true)
1132  endfunction
1133
1134" Open preview window. Window is open in:
1135"   - Floating window on Neovim (0.4.0 or later)
1136"   - Preview window on Neovim (0.3.0 or earlier) or Vim
1137function! s:OpenHoverPreview(lines, filetype) abort
1138    " Use local variable since parameter is not modifiable
1139    let lines = a:lines
1140    let bufnr = bufnr('%')
1141
1142    let use_float_win = s:ShouldUseFloatWindow()
1143    if use_float_win
1144      let pos = getpos('.')
1145
1146      " Calculate width and height
1147      let width = 0
1148      for index in range(len(lines))
1149        let line = lines[index]
1150        let lw = strdisplaywidth(line)
1151        if lw > width
1152          let width = lw
1153        endif
1154        let lines[index] = line
1155      endfor
1156
1157      let height = len(lines)
1158
1159      " Calculate anchor
1160      " Prefer North, but if there is no space, fallback into South
1161      let bottom_line = line('w0') + winheight(0) - 1
1162      if pos[1] + height <= bottom_line
1163        let vert = 'N'
1164        let row = 1
1165      else
1166        let vert = 'S'
1167        let row = 0
1168      endif
1169
1170      " Prefer West, but if there is no space, fallback into East
1171      if pos[2] + width <= &columns
1172        let hor = 'W'
1173        let col = 0
1174      else
1175        let hor = 'E'
1176        let col = 1
1177      endif
1178
1179      let buf = nvim_create_buf(v:false, v:true)
1180      call nvim_buf_set_lines(buf, 0, -1, v:true, lines)
1181      " using v:true for second argument of nvim_open_win make the floating
1182      " window disappear
1183      let float_win_id = nvim_open_win(buf, v:false, {
1184            \   'relative': 'cursor',
1185            \   'anchor': vert . hor,
1186            \   'row': row,
1187            \   'col': col,
1188            \   'width': width,
1189            \   'height': height,
1190            \   'style': 'minimal',
1191            \ })
1192
1193      if a:filetype isnot v:null
1194        call nvim_win_set_option(float_win_id, 'filetype', a:filetype)
1195      endif
1196
1197      call nvim_buf_set_option(buf, 'modified', v:false)
1198      call nvim_buf_set_option(buf, 'modifiable', v:false)
1199
1200      " Unlike preview window, :pclose does not close window. Instead, close
1201      " hover window automatically when cursor is moved.
1202      let call_after_move = printf('<SID>CloseFloatingHoverOnCursorMove(%d, %s)', float_win_id, string(pos))
1203      let call_on_bufenter = printf('<SID>CloseFloatingHoverOnBufEnter(%d, %d)', float_win_id, bufnr)
1204      augroup nvim_termdebug_close_hover
1205        execute 'autocmd CursorMoved,CursorMovedI,InsertEnter <buffer> call ' . call_after_move
1206        execute 'autocmd BufEnter * call ' . call_on_bufenter
1207      augroup END
1208    else
1209      echomsg a:lines[0]
1210    endif
1211endfunction
1212
1213" Handle an error.
1214func s:HandleError(msg)
1215  if s:ignoreEvalError
1216    " Result of s:SendEval() failed, ignore.
1217    let s:ignoreEvalError = 0
1218    let s:evalFromBalloonExpr = 0
1219    return
1220  endif
1221  let msgVal = substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
1222  echoerr substitute(msgVal, '\\"', '"', 'g')
1223endfunc
1224
1225func s:GotoSourcewinOrCreateIt()
1226  if !win_gotoid(s:sourcewin)
1227    new
1228    let s:sourcewin = win_getid(winnr())
1229    call s:InstallWinbar()
1230  endif
1231endfunc
1232
1233func s:GotoAsmwinOrCreateIt()
1234  if !win_gotoid(s:asmwin)
1235    if win_gotoid(s:sourcewin)
1236      exe 'rightbelow new'
1237    else
1238      exe 'new'
1239    endif
1240
1241    let s:asmwin = win_getid(winnr())
1242
1243    setlocal nowrap
1244    setlocal number
1245    setlocal noswapfile
1246    setlocal buftype=nofile
1247    setlocal modifiable
1248
1249    let asmbuf = bufnr('Termdebug-asm-listing')
1250    if asmbuf > 0
1251      exe 'buffer' . asmbuf
1252    else
1253      exe 'file Termdebug-asm-listing'
1254    endif
1255
1256    if exists('g:termdebug_disasm_window')
1257      if g:termdebug_disasm_window > 1
1258        exe 'resize ' . g:termdebug_disasm_window
1259      endif
1260    endif
1261  endif
1262
1263  if s:asm_addr != ''
1264    let lnum = search('^' . s:asm_addr)
1265    if lnum == 0
1266      if s:stopped
1267        call s:SendCommand('disassemble $pc')
1268      endif
1269    else
1270      exe 'sign unplace ' . s:asm_id
1271      exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC'
1272    endif
1273  endif
1274endfunc
1275
1276" Handle stopping and running message from gdb.
1277" Will update the sign that shows the current position.
1278func s:HandleCursor(msg)
1279  let wid = win_getid(winnr())
1280
1281  if a:msg =~ '^\*stopped'
1282    "call ch_log('program stopped')
1283    let s:stopped = 1
1284  elseif a:msg =~ '^\*running'
1285    "call ch_log('program running')
1286    let s:stopped = 0
1287  endif
1288
1289  if a:msg =~ 'fullname='
1290    let fname = s:GetFullname(a:msg)
1291  else
1292    let fname = ''
1293  endif
1294
1295  if a:msg =~ 'addr='
1296    let asm_addr = s:GetAsmAddr(a:msg)
1297    if asm_addr != ''
1298      let s:asm_addr = asm_addr
1299
1300      let curwinid = win_getid(winnr())
1301      if win_gotoid(s:asmwin)
1302      let lnum = search('^' . s:asm_addr)
1303      if lnum == 0
1304        call s:SendCommand('disassemble $pc')
1305      else
1306        exe 'sign unplace ' . s:asm_id
1307        exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC'
1308      endif
1309
1310      call win_gotoid(curwinid)
1311      endif
1312    endif
1313  endif
1314
1315  if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
1316    let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
1317    if lnum =~ '^[0-9]*$'
1318      call s:GotoSourcewinOrCreateIt()
1319      if expand('%:p') != fnamemodify(fname, ':p')
1320        if &modified
1321          " TODO: find existing window
1322          exe 'split ' . fnameescape(fname)
1323          let s:sourcewin = win_getid(winnr())
1324          call s:InstallWinbar()
1325        else
1326          exe 'edit ' . fnameescape(fname)
1327        endif
1328      endif
1329      exe lnum
1330      normal! zv
1331      exe 'sign unplace ' . s:pc_id
1332      exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname
1333      if !exists('b:save_signcolumn')
1334        let b:save_signcolumn = &signcolumn
1335        call add(s:signcolumn_buflist, bufnr())
1336      endif
1337      setlocal signcolumn=yes
1338    endif
1339  elseif !s:stopped || fname != ''
1340    exe 'sign unplace ' . s:pc_id
1341  endif
1342
1343  call win_gotoid(wid)
1344endfunc
1345
1346let s:BreakpointSigns = []
1347
1348func s:CreateBreakpoint(id, subid, enabled)
1349  let nr = printf('%d.%d', a:id, a:subid)
1350  if index(s:BreakpointSigns, nr) == -1
1351    call add(s:BreakpointSigns, nr)
1352    if a:enabled == "n"
1353      let hiName = "debugBreakpointDisabled"
1354    else
1355      let hiName = "debugBreakpoint"
1356    endif
1357    exe "sign define debugBreakpoint" . nr . " text=" . substitute(nr, '\..*', '', '') . " texthl=" . hiName
1358  endif
1359endfunc
1360
1361func! s:SplitMsg(s)
1362  return split(a:s, '{.\{-}}\zs')
1363endfunction
1364
1365" Handle setting a breakpoint
1366" Will update the sign that shows the breakpoint
1367func s:HandleNewBreakpoint(msg, modifiedFlag)
1368  if a:msg !~ 'fullname='
1369    " a watch or a pending breakpoint does not have a file name
1370    if a:msg =~ 'pending='
1371      let nr = substitute(a:msg, '.*number=\"\([0-9.]*\)\".*', '\1', '')
1372      let target = substitute(a:msg, '.*pending=\"\([^"]*\)\".*', '\1', '')
1373      echomsg 'Breakpoint ' . nr . ' (' . target  . ') pending.'
1374    endif
1375    return
1376  endif
1377  for msg in s:SplitMsg(a:msg)
1378    let fname = s:GetFullname(msg)
1379    if empty(fname)
1380      continue
1381    endif
1382    let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '')
1383    if empty(nr)
1384      return
1385    endif
1386
1387    " If "nr" is 123 it becomes "123.0" and subid is "0".
1388    " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded.
1389    let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0')
1390    let enabled = substitute(msg, '.*enabled="\([yn]\)".*', '\1', '')
1391    call s:CreateBreakpoint(id, subid, enabled)
1392
1393    if has_key(s:breakpoints, id)
1394      let entries = s:breakpoints[id]
1395    else
1396      let entries = {}
1397      let s:breakpoints[id] = entries
1398    endif
1399    if has_key(entries, subid)
1400      let entry = entries[subid]
1401    else
1402      let entry = {}
1403      let entries[subid] = entry
1404    endif
1405
1406    let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '')
1407    let entry['fname'] = fname
1408    let entry['lnum'] = lnum
1409
1410    let bploc = printf('%s:%d', fname, lnum)
1411    if !has_key(s:breakpoint_locations, bploc)
1412      let s:breakpoint_locations[bploc] = []
1413    endif
1414    let s:breakpoint_locations[bploc] += [id]
1415
1416    if bufloaded(fname)
1417      call s:PlaceSign(id, subid, entry)
1418      let posMsg = ' at line ' . lnum . '.'
1419    else
1420      let posMsg = ' in ' . fname . ' at line ' . lnum . '.'
1421    endif
1422    if !a:modifiedFlag
1423      let actionTaken = 'created'
1424    elseif enabled == 'n'
1425      let actionTaken = 'disabled'
1426    else
1427      let actionTaken = 'enabled'
1428    endif
1429    echomsg 'Breakpoint ' . nr . ' ' . actionTaken . posMsg
1430  endfor
1431endfunc
1432
1433func s:PlaceSign(id, subid, entry)
1434  let nr = printf('%d.%d', a:id, a:subid)
1435  exe 'sign place ' . s:Breakpoint2SignNumber(a:id, a:subid) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . nr . ' priority=110 file=' . a:entry['fname']
1436  let a:entry['placed'] = 1
1437endfunc
1438
1439" Handle deleting a breakpoint
1440" Will remove the sign that shows the breakpoint
1441func s:HandleBreakpointDelete(msg)
1442  let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
1443  if empty(id)
1444    return
1445  endif
1446  if has_key(s:breakpoints, id)
1447    for [subid, entry] in items(s:breakpoints[id])
1448      if has_key(entry, 'placed')
1449        exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
1450        unlet entry['placed']
1451      endif
1452    endfor
1453    unlet s:breakpoints[id]
1454    echomsg 'Breakpoint ' . id . ' cleared.'
1455  endif
1456endfunc
1457
1458" Handle the debugged program starting to run.
1459" Will store the process ID in s:pid
1460func s:HandleProgramRun(msg)
1461  let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0
1462  if nr == 0
1463    return
1464  endif
1465  let s:pid = nr
1466  "call ch_log('Detected process ID: ' . s:pid)
1467endfunc
1468
1469" Handle a BufRead autocommand event: place any signs.
1470func s:BufRead()
1471  let fname = expand('<afile>:p')
1472  for [id, entries] in items(s:breakpoints)
1473    for [subid, entry] in items(entries)
1474      if entry['fname'] == fname
1475        call s:PlaceSign(id, subid, entry)
1476      endif
1477    endfor
1478  endfor
1479endfunc
1480
1481" Handle a BufUnloaded autocommand event: unplace any signs.
1482func s:BufUnloaded()
1483  let fname = expand('<afile>:p')
1484  for [id, entries] in items(s:breakpoints)
1485    for [subid, entry] in items(entries)
1486      if entry['fname'] == fname
1487        let entry['placed'] = 0
1488      endif
1489    endfor
1490  endfor
1491endfunc
1492
1493let &cpo = s:keepcpo
1494unlet s:keepcpo
1495