neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

termdebug.vim (57117B)


      1 " Debugger plugin using gdb.
      2 "
      3 " Author: Bram Moolenaar
      4 " Copyright: Vim license applies, see ":help license"
      5 " Last Change: 2025 Jul 08
      6 "
      7 " WORK IN PROGRESS - The basics works stable, more to come
      8 " Note: In general you need at least GDB 7.12 because this provides the
      9 " frame= response in MI thread-selected events we need to sync stack to file.
     10 " The one included with "old" MingW is too old (7.6.1), you may upgrade it or
     11 " use a newer version from http://www.equation.com/servlet/equation.cmd?fa=gdb
     12 "
     13 " There are two ways to run gdb:
     14 " - In a terminal window; used if possible, does not work on MS-Windows
     15 "   Not used when g:termdebug_use_prompt is set to 1.
     16 " - Using a "prompt" buffer; may use a terminal window for the program
     17 "
     18 " For both the current window is used to view source code and shows the
     19 " current statement from gdb.
     20 "
     21 " USING A TERMINAL WINDOW
     22 "
     23 " Opens two visible terminal windows:
     24 " 1. runs a pty for the debugged program, as with ":term NONE"
     25 " 2. runs gdb, passing the pty of the debugged program
     26 " A third terminal window is hidden, it is used for communication with gdb.
     27 "
     28 " USING A PROMPT BUFFER
     29 "
     30 " Opens a window with a prompt buffer to communicate with gdb.
     31 " Gdb is run as a job with callbacks for I/O.
     32 " On Unix another terminal window is opened to run the debugged program
     33 " On MS-Windows a separate console is opened to run the debugged program
     34 "
     35 " The communication with gdb uses GDB/MI.  See:
     36 " https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html
     37 "
     38 " NEOVIM COMPATIBILITY
     39 "
     40 " The vim specific functionalities were replaced with neovim specific calls:
     41 " - term_start -> `jobstart(…, {'term': v:true})`
     42 " - term_sendkeys -> chansend
     43 " - term_getline -> getbufline
     44 " - job_info && term_getjob -> nvim_get_chan_info
     45 " - balloon -> vim.lsp.util.open_floating_preview
     46 
     47 func s:Echoerr(msg)
     48  echohl ErrorMsg | echom $'[termdebug] {a:msg}' | echohl None
     49 endfunc
     50 
     51 " In case this gets sourced twice.
     52 if exists(':Termdebug')
     53  call s:Echoerr('Termdebug already loaded.')
     54  finish
     55 endif
     56 
     57 " The terminal feature does not work with gdb on win32.
     58 if !has('win32')
     59  let s:way = 'terminal'
     60 else
     61  let s:way = 'prompt'
     62 endif
     63 
     64 let s:keepcpo = &cpo
     65 set cpo&vim
     66 
     67 " The command that starts debugging, e.g. ":Termdebug vim".
     68 " To end type "quit" in the gdb window.
     69 command -nargs=* -complete=file -bang Termdebug call s:StartDebug(<bang>0, <f-args>)
     70 command -nargs=+ -complete=file -bang TermdebugCommand call s:StartDebugCommand(<bang>0, <f-args>)
     71 
     72 let s:pc_id = 12
     73 let s:asm_id = 13
     74 let s:break_id = 14  " breakpoint number is added to this
     75 let s:stopped = v:true
     76 let s:running = v:false
     77 
     78 let s:parsing_disasm_msg = 0
     79 let s:asm_lines = []
     80 let s:asm_addr = ''
     81 
     82 " Take a breakpoint number as used by GDB and turn it into an integer.
     83 " The breakpoint may contain a dot: 123.4 -> 123004
     84 " The main breakpoint has a zero subid.
     85 func s:Breakpoint2SignNumber(id, subid)
     86  return s:break_id + a:id * 1000 + a:subid
     87 endfunction
     88 
     89 " Define or adjust the default highlighting, using background "new".
     90 " When the 'background' option is set then "old" has the old value.
     91 func s:Highlight(init, old, new)
     92  let default = a:init ? 'default ' : ''
     93  if a:new ==# 'light' && a:old !=# 'light'
     94    exe $"hi {default}debugPC term=reverse ctermbg=lightblue guibg=lightblue"
     95  elseif a:new ==# 'dark' && a:old !=# 'dark'
     96    exe $"hi {default}debugPC term=reverse ctermbg=darkblue guibg=darkblue"
     97  endif
     98 endfunc
     99 
    100 " Define the default highlighting, using the current 'background' value.
    101 func s:InitHighlight()
    102  call s:Highlight(1, '', &background)
    103  hi default debugBreakpoint term=reverse ctermbg=red guibg=red
    104  hi default debugBreakpointDisabled term=reverse ctermbg=gray guibg=gray
    105 endfunc
    106 
    107 " Setup an autocommand to redefine the default highlight when the colorscheme
    108 " is changed.
    109 func s:InitAutocmd()
    110  augroup TermDebug
    111    autocmd!
    112    autocmd ColorScheme * call s:InitHighlight()
    113  augroup END
    114 endfunc
    115 
    116 " Get the command to execute the debugger as a list, defaults to ["gdb"].
    117 func s:GetCommand()
    118  if exists('g:termdebug_config')
    119    let cmd = get(g:termdebug_config, 'command', 'gdb')
    120  elseif exists('g:termdebugger')
    121    let cmd = g:termdebugger
    122  else
    123    let cmd = 'gdb'
    124  endif
    125 
    126  return type(cmd) == v:t_list ? copy(cmd) : [cmd]
    127 endfunc
    128 
    129 func s:StartDebug(bang, ...)
    130  " First argument is the command to debug, second core file or process ID.
    131  call s:StartDebug_internal({'gdb_args': a:000, 'bang': a:bang})
    132 endfunc
    133 
    134 func s:StartDebugCommand(bang, ...)
    135  " First argument is the command to debug, rest are run arguments.
    136  call s:StartDebug_internal({'gdb_args': [a:1], 'proc_args': a:000[1:], 'bang': a:bang})
    137 endfunc
    138 
    139 func s:StartDebug_internal(dict)
    140  if exists('s:gdbwin')
    141    call s:Echoerr('Terminal debugger already running, cannot run two')
    142    return
    143  endif
    144  let gdbcmd = s:GetCommand()
    145  if !executable(gdbcmd[0])
    146    call s:Echoerr($'Cannot execute debugger program "{gdbcmd[0]}"')
    147    return
    148  endif
    149 
    150  let s:ptywin = 0
    151  let s:pid = 0
    152  let s:asmwin = 0
    153  let s:asmbufnr = 0
    154  let s:varwin = 0
    155  let s:varbufnr = 0
    156 
    157  if exists('#User#TermdebugStartPre')
    158    doauto <nomodeline> User TermdebugStartPre
    159  endif
    160 
    161  " Uncomment this line to write logging in "debuglog".
    162  " call ch_logfile('debuglog', 'w')
    163 
    164  let s:sourcewin = win_getid()
    165 
    166  " Remember the old value of 'signcolumn' for each buffer that it's set in, so
    167  " that we can restore the value for all buffers.
    168  let b:save_signcolumn = &signcolumn
    169  let s:signcolumn_buflist = [bufnr()]
    170 
    171  let s:saved_columns = 0
    172  let s:allleft = v:false
    173  let wide = 0
    174  if exists('g:termdebug_config')
    175    let wide = get(g:termdebug_config, 'wide', 0)
    176  elseif exists('g:termdebug_wide')
    177    let wide = g:termdebug_wide
    178  endif
    179  if wide > 0
    180    if &columns < wide
    181      let s:saved_columns = &columns
    182      let &columns = wide
    183      " If we make the Vim window wider, use the whole left half for the debug
    184      " windows.
    185      let s:allleft = v:true
    186    endif
    187    let s:vertical = v:true
    188  else
    189    let s:vertical = v:false
    190  endif
    191 
    192  " Override using a terminal window by setting g:termdebug_use_prompt to 1.
    193  let use_prompt = 0
    194  if exists('g:termdebug_config')
    195    let use_prompt = get(g:termdebug_config, 'use_prompt', 0)
    196  elseif exists('g:termdebug_use_prompt')
    197    let use_prompt = g:termdebug_use_prompt
    198  endif
    199  if !has('win32') && !use_prompt
    200    let s:way = 'terminal'
    201  else
    202    let s:way = 'prompt'
    203  endif
    204 
    205  if s:way == 'prompt'
    206    call s:StartDebug_prompt(a:dict)
    207  else
    208    call s:StartDebug_term(a:dict)
    209  endif
    210 
    211  if s:GetDisasmWindow()
    212    let curwinid = win_getid()
    213    call s:GotoAsmwinOrCreateIt()
    214    call win_gotoid(curwinid)
    215  endif
    216 
    217  if s:GetVariablesWindow()
    218    let curwinid = win_getid()
    219    call s:GotoVariableswinOrCreateIt()
    220    call win_gotoid(curwinid)
    221  endif
    222 
    223  if exists('#User#TermdebugStartPost')
    224    doauto <nomodeline> User TermdebugStartPost
    225  endif
    226 endfunc
    227 
    228 " Use when debugger didn't start or ended.
    229 func s:CloseBuffers()
    230  exe $'bwipe! {s:ptybufnr}'
    231  if s:asmbufnr > 0 && bufexists(s:asmbufnr)
    232    exe $'bwipe! {s:asmbufnr}'
    233  endif
    234  if s:varbufnr > 0 && bufexists(s:varbufnr)
    235    exe $'bwipe! {s:varbufnr}'
    236  endif
    237  let s:running = v:false
    238  unlet! s:gdbwin
    239 endfunc
    240 
    241 func s:IsGdbStarted()
    242  if !s:gdb_running
    243    let cmd_name = string(s:GetCommand()[0])
    244    call s:Echoerr($'{cmd_name} exited unexpectedly')
    245    call s:CloseBuffers()
    246    return v:false
    247  endif
    248  return v:true
    249 endfunc
    250 
    251 " Open a terminal window without a job, to run the debugged program in.
    252 func s:StartDebug_term(dict)
    253  execute s:vertical ? 'vnew' : 'new'
    254  let s:pty_job_id = jobstart('tail -f /dev/null;#gdb program', {'term': v:true})
    255  if s:pty_job_id == 0
    256    call s:Echoerr('Invalid argument (or job table is full) while opening terminal window')
    257    return
    258  elseif s:pty_job_id == -1
    259    call s:Echoerr('Failed to open the program terminal window')
    260    return
    261  endif
    262  let pty_job_info = nvim_get_chan_info(s:pty_job_id)
    263  let s:ptybufnr = pty_job_info['buffer']
    264  let pty = pty_job_info['pty']
    265  let s:ptywin = win_getid()
    266  if s:vertical
    267    " Assuming the source code window will get a signcolumn, use two more
    268    " columns for that, thus one less for the terminal window.
    269    exe $":{(&columns / 2 - 1)}wincmd |"
    270    if s:allleft
    271      " use the whole left column
    272      wincmd H
    273    endif
    274  endif
    275 
    276  " Create a hidden terminal window to communicate with gdb
    277  let s:comm_job_id = jobstart('tail -f /dev/null;#gdb communication', {
    278        \ 'on_stdout': function('s:JobOutCallback', {'last_line': '', 'real_cb': function('s:CommOutput')}),
    279        \ 'pty': v:true,
    280        \ })
    281  " hide terminal buffer
    282  if s:comm_job_id == 0
    283    call s:Echoerr('Invalid argument (or job table is full) while opening communication terminal window')
    284    exe 'bwipe! ' . s:ptybufnr
    285    return
    286  elseif s:comm_job_id == -1
    287    call s:Echoerr('Failed to open the communication terminal window')
    288    exe $'bwipe! {s:ptybufnr}'
    289    return
    290  endif
    291  let comm_job_info = nvim_get_chan_info(s:comm_job_id)
    292  let commpty = comm_job_info['pty']
    293 
    294  let gdb_args = get(a:dict, 'gdb_args', [])
    295  let proc_args = get(a:dict, 'proc_args', [])
    296 
    297  let gdb_cmd = s:GetCommand()
    298 
    299  if exists('g:termdebug_config') && has_key(g:termdebug_config, 'command_add_args')
    300    let gdb_cmd = g:termdebug_config.command_add_args(gdb_cmd, pty)
    301  else
    302    " Add -quiet to avoid the intro message causing a hit-enter prompt.
    303    let gdb_cmd += ['-quiet']
    304    " Disable pagination, it causes everything to stop at the gdb
    305    let gdb_cmd += ['-iex', 'set pagination off']
    306    " Interpret commands while the target is running.  This should usually only
    307    " be exec-interrupt, since many commands don't work properly while the
    308    " target is running (so execute during startup).
    309    let gdb_cmd += ['-iex', 'set mi-async on']
    310    " Open a terminal window to run the debugger.
    311    let gdb_cmd += ['-tty', pty]
    312    " Command executed _after_ startup is done, provides us with the necessary
    313    " feedback
    314    let gdb_cmd += ['-ex', 'echo startupdone\n']
    315  endif
    316 
    317  if exists('g:termdebug_config') && has_key(g:termdebug_config, 'command_filter')
    318    let gdb_cmd = g:termdebug_config.command_filter(gdb_cmd)
    319  endif
    320 
    321  " Adding arguments requested by the user
    322  let gdb_cmd += gdb_args
    323 
    324  execute 'new'
    325  " call ch_log($'executing "{join(gdb_cmd)}"')
    326  let s:gdb_job_id = jobstart(gdb_cmd, {'term': v:true, 'on_exit': function('s:EndTermDebug')})
    327  if s:gdb_job_id == 0
    328    call s:Echoerr('Invalid argument (or job table is full) while opening gdb terminal window')
    329    exe 'bwipe! ' . s:ptybufnr
    330    return
    331  elseif s:gdb_job_id == -1
    332    call s:Echoerr('Failed to open the gdb terminal window')
    333    call s:CloseBuffers()
    334    return
    335  endif
    336  let s:gdb_running = v:true
    337  let s:starting = v:true
    338  let gdb_job_info = nvim_get_chan_info(s:gdb_job_id)
    339  let s:gdbbufnr = gdb_job_info['buffer']
    340  let s:gdbwin = win_getid()
    341 
    342  " Wait for the "startupdone" message before sending any commands.
    343  let try_count = 0
    344  while 1
    345    if !s:IsGdbStarted()
    346      return
    347    endif
    348 
    349    for lnum in range(1, 200)
    350      if get(getbufline(s:gdbbufnr, lnum), 0, '') =~ 'startupdone'
    351        let try_count = 9999
    352        break
    353      endif
    354    endfor
    355    let try_count += 1
    356    if try_count > 300
    357      " done or give up after five seconds
    358      break
    359    endif
    360    sleep 10m
    361  endwhile
    362 
    363  " Set arguments to be run.
    364  if !empty(proc_args)
    365    call chansend(s:gdb_job_id, $"server set args {join(proc_args)}\r")
    366  endif
    367 
    368  " Connect gdb to the communication pty, using the GDB/MI interface.
    369  " Prefix "server" to avoid adding this to the history.
    370  call chansend(s:gdb_job_id, $"server new-ui mi {commpty}\r")
    371 
    372  " Wait for the response to show up, users may not notice the error and wonder
    373  " why the debugger doesn't work.
    374  let try_count = 0
    375  while 1
    376    if !s:IsGdbStarted()
    377      return
    378    endif
    379 
    380    let response = ''
    381    for lnum in range(1, 200)
    382      let line1 = get(getbufline(s:gdbbufnr, lnum), 0, '')
    383      let line2 = get(getbufline(s:gdbbufnr, lnum + 1), 0, '')
    384      if line1 =~ 'new-ui mi '
    385        " response can be in the same line or the next line
    386        let response = line1 . line2
    387        if response =~ 'Undefined command'
    388          call s:Echoerr('Sorry, your gdb is too old, gdb 7.12 is required')
    389          " CHECKME: possibly send a "server show version" here
    390          call s:CloseBuffers()
    391          return
    392        endif
    393        if response =~ 'New UI allocated'
    394          " Success!
    395          break
    396        endif
    397      elseif line1 =~ 'Reading symbols from' && line2 !~ 'new-ui mi '
    398        " Reading symbols might take a while, try more times
    399        let try_count -= 1
    400      endif
    401    endfor
    402    if response =~ 'New UI allocated'
    403      break
    404    endif
    405    let try_count += 1
    406    if try_count > 100
    407      call s:Echoerr('Cannot check if your gdb works, continuing anyway')
    408      break
    409    endif
    410    sleep 10m
    411  endwhile
    412 
    413  let s:starting = v:false
    414 
    415  " Set the filetype, this can be used to add mappings.
    416  set filetype=termdebug
    417 
    418  call s:StartDebugCommon(a:dict)
    419 endfunc
    420 
    421 " Open a window with a prompt buffer to run gdb in.
    422 func s:StartDebug_prompt(dict)
    423  if s:vertical
    424    vertical new
    425  else
    426    new
    427  endif
    428  let s:gdbwin = win_getid()
    429  let s:promptbuf = bufnr('')
    430  call prompt_setprompt(s:promptbuf, 'gdb> ')
    431  set buftype=prompt
    432 
    433  if empty(glob('gdb'))
    434    file gdb
    435  elseif empty(glob('Termdebug-gdb-console'))
    436    file Termdebug-gdb-console
    437  else
    438    call s:Echoerr("You have a file/folder named 'gdb'
    439          \ or 'Termdebug-gdb-console'.
    440          \ Please exit and rename them because Termdebug may not work as expected.")
    441  endif
    442 
    443  call prompt_setcallback(s:promptbuf, function('s:PromptCallback'))
    444  call prompt_setinterrupt(s:promptbuf, function('s:PromptInterrupt'))
    445 
    446  if s:vertical
    447    " Assuming the source code window will get a signcolumn, use two more
    448    " columns for that, thus one less for the terminal window.
    449    exe $":{(&columns / 2 - 1)}wincmd |"
    450  endif
    451 
    452  let gdb_args = get(a:dict, 'gdb_args', [])
    453  let proc_args = get(a:dict, 'proc_args', [])
    454 
    455  let gdb_cmd = s:GetCommand()
    456  " Add -quiet to avoid the intro message causing a hit-enter prompt.
    457  let gdb_cmd += ['-quiet']
    458  " Disable pagination, it causes everything to stop at the gdb, needs to be run early
    459  let gdb_cmd += ['-iex', 'set pagination off']
    460  " Interpret commands while the target is running.  This should usually only
    461  " be exec-interrupt, since many commands don't work properly while the
    462  " target is running (so execute during startup).
    463  let gdb_cmd += ['-iex', 'set mi-async on']
    464  " directly communicate via mi2
    465  let gdb_cmd += ['--interpreter=mi2']
    466 
    467  " Adding arguments requested by the user
    468  let gdb_cmd += gdb_args
    469 
    470  " call ch_log($'executing "{join(gdb_cmd)}"')
    471  let s:gdbjob = jobstart(gdb_cmd, {
    472        \ 'on_exit': function('s:EndPromptDebug'),
    473        \ 'on_stdout': function('s:JobOutCallback', {'last_line': '', 'real_cb': function('s:GdbOutCallback')}),
    474        \ })
    475  if s:gdbjob == 0
    476    call s:Echoerr('Invalid argument (or job table is full) while starting gdb job')
    477    exe $'bwipe! {s:ptybufnr}'
    478    return
    479  elseif s:gdbjob == -1
    480    call s:Echoerr('Failed to start the gdb job')
    481    call s:CloseBuffers()
    482    return
    483  endif
    484  exe $'au BufUnload <buffer={s:promptbuf}> ++once call jobstop(s:gdbjob)'
    485 
    486  let s:ptybufnr = 0
    487  if has('win32')
    488    " MS-Windows: run in a new console window for maximum compatibility
    489    call s:SendCommand('set new-console on')
    490  else
    491    " Unix: Run the debugged program in a terminal window.  Open it below the
    492    " gdb window.
    493    belowright new
    494    let s:pty_job_id = jobstart('tail -f /dev/null;#gdb program', {'term': v:true})
    495    if s:pty_job_id == 0
    496      call s:Echoerr('Invalid argument (or job table is full) while opening terminal window')
    497      return
    498    elseif s:pty_job_id == -1
    499      call s:Echoerr('Failed to open the program terminal window')
    500      return
    501    endif
    502    let pty_job_info = nvim_get_chan_info(s:pty_job_id)
    503    let s:ptybufnr = pty_job_info['buffer']
    504    let pty = pty_job_info['pty']
    505    let s:ptywin = win_getid()
    506    call s:SendCommand($'tty {pty}')
    507 
    508    " Since GDB runs in a prompt window, the environment has not been set to
    509    " match a terminal window, need to do that now.
    510    call s:SendCommand('set env TERM = xterm-color')
    511    call s:SendCommand($'set env ROWS = {winheight(s:ptywin)}')
    512    call s:SendCommand($'set env LINES = {winheight(s:ptywin)}')
    513    call s:SendCommand($'set env COLUMNS = {winwidth(s:ptywin)}')
    514    call s:SendCommand($'set env COLORS = {&t_Co}')
    515    call s:SendCommand($'set env VIM_TERMINAL = {v:version}')
    516  endif
    517  call s:SendCommand('set print pretty on')
    518  call s:SendCommand('set breakpoint pending on')
    519 
    520  " Set arguments to be run
    521  if !empty(proc_args)
    522    call s:SendCommand($'set args {join(proc_args)}')
    523  endif
    524 
    525  call s:StartDebugCommon(a:dict)
    526  startinsert
    527 endfunc
    528 
    529 func s:StartDebugCommon(dict)
    530  " Sign used to highlight the line where the program has stopped.
    531  " There can be only one.
    532  call sign_define('debugPC', #{linehl: 'debugPC'})
    533 
    534  " Install debugger commands in the text window.
    535  call win_gotoid(s:sourcewin)
    536  call s:InstallCommands()
    537  call win_gotoid(s:gdbwin)
    538 
    539  " Contains breakpoints that have been placed, key is a string with the GDB
    540  " breakpoint number.
    541  " Each entry is a dict, containing the sub-breakpoints.  Key is the subid.
    542  " For a breakpoint that is just a number the subid is zero.
    543  " For a breakpoint "123.4" the id is "123" and subid is "4".
    544  " Example, when breakpoint "44", "123", "123.1" and "123.2" exist:
    545  " {'44': {'0': entry}, '123': {'0': entry, '1': entry, '2': entry}}
    546  let s:breakpoints = {}
    547 
    548  " Contains breakpoints by file/lnum.  The key is "fname:lnum".
    549  " Each entry is a list of breakpoint IDs at that position.
    550  let s:breakpoint_locations = {}
    551 
    552  augroup TermDebug
    553    au BufRead * call s:BufRead()
    554    au BufUnload * call s:BufUnloaded()
    555    au OptionSet background call s:Highlight(0, v:option_old, v:option_new)
    556  augroup END
    557 
    558  " Run the command if the bang attribute was given and got to the debug
    559  " window.
    560  if get(a:dict, 'bang', 0)
    561    call s:SendResumingCommand('-exec-run')
    562    call win_gotoid(s:ptywin)
    563  endif
    564 endfunc
    565 
    566 " Send a command to gdb.  "cmd" is the string without line terminator.
    567 func s:SendCommand(cmd)
    568  " call ch_log($'sending to gdb: {a:cmd}')
    569  if s:way == 'prompt'
    570    call chansend(s:gdbjob, $"{a:cmd}\n")
    571  else
    572    call chansend(s:comm_job_id, $"{a:cmd}\r")
    573  endif
    574 endfunc
    575 
    576 " This is global so that a user can create their mappings with this.
    577 func TermDebugSendCommand(cmd)
    578  if s:way == 'prompt'
    579    call chansend(s:gdbjob, $"{a:cmd}\n")
    580  else
    581    let do_continue = 0
    582    if !s:stopped
    583      let do_continue = 1
    584      if s:way == 'prompt'
    585        " Need to send a signal to get the UI to listen.  Strangely this is only
    586        " needed once.
    587        call jobstop(s:gdbjob)
    588      else
    589        Stop
    590      endif
    591      sleep 10m
    592    endif
    593    " TODO: should we prepend CTRL-U to clear the command?
    594    call chansend(s:gdb_job_id, $"{a:cmd}\r")
    595    if do_continue
    596      Continue
    597    endif
    598  endif
    599 endfunc
    600 
    601 " Send a command that resumes the program.  If the program isn't stopped the
    602 " command is not sent (to avoid a repeated command to cause trouble).
    603 " If the command is sent then reset s:stopped.
    604 func s:SendResumingCommand(cmd)
    605  if s:stopped
    606    " reset s:stopped here, it may take a bit of time before we get a response
    607    let s:stopped = v:false
    608    " call ch_log('assume that program is running after this command')
    609    call s:SendCommand(a:cmd)
    610  " else
    611    " call ch_log($'dropping command, program is running: {a:cmd}')
    612  endif
    613 endfunc
    614 
    615 " Function called when entering a line in the prompt buffer.
    616 func s:PromptCallback(text)
    617  call s:SendCommand(a:text)
    618 endfunc
    619 
    620 " Function called when pressing CTRL-C in the prompt buffer and when placing a
    621 " breakpoint.
    622 func s:PromptInterrupt()
    623  " call ch_log('Interrupting gdb')
    624  if has('win32')
    625    " Using job_stop() does not work on MS-Windows, need to send SIGTRAP to
    626    " the debugger program so that gdb responds again.
    627    if s:pid == 0
    628      call s:Echoerr('Cannot interrupt gdb, did not find a process ID')
    629    else
    630      call debugbreak(s:pid)
    631    endif
    632  else
    633    call v:lua.vim.uv.kill(jobpid(s:gdbjob), 'sigint')
    634  endif
    635 endfunc
    636 
    637 " Wrapper around job callback that handles partial lines (:h channel-lines).
    638 " It should be called from a Dictionary with the following keys:
    639 " - last_line: the last (partial) line received
    640 " - real_cb: a callback that assumes full lines
    641 func s:JobOutCallback(jobid, data, event) dict
    642  let eof = (a:data == [''])
    643  let msgs = a:data
    644  let msgs[0] = self.last_line .. msgs[0]
    645  if eof
    646    let self.last_line = ''
    647  else
    648    let self.last_line = msgs[-1]
    649    unlet msgs[-1]
    650  endif
    651  call self.real_cb(a:jobid, msgs, a:event)
    652 endfunc
    653 
    654 " Function called when gdb outputs text.
    655 func s:GdbOutCallback(job_id, msgs, event)
    656  " call ch_log($'received from gdb: {a:text}')
    657 
    658  let comm_msgs = []
    659  let lines = []
    660 
    661  for msg in a:msgs
    662    " Disassembly messages need to be forwarded as-is.
    663    if s:parsing_disasm_msg || msg =~ '^&"disassemble'
    664      call s:CommOutput(a:job_id, [msg], a:event)
    665      continue
    666    endif
    667 
    668    " Drop the gdb prompt, we have our own.
    669    " Drop status and echo'd commands.
    670    if msg == '(gdb) ' || msg == '^done' || msg[0] == '&'
    671      continue
    672    endif
    673 
    674    if msg =~ '^\^error,msg='
    675      if exists('s:evalexpr')
    676            \ && s:DecodeMessage(msg[11:], v:false)
    677            \    =~ 'A syntax error in expression, near\|No symbol .* in current context'
    678        " Silently drop evaluation errors.
    679        unlet s:evalexpr
    680        continue
    681      endif
    682    elseif msg[0] == '~'
    683      call add(lines, s:DecodeMessage(msg[1:], v:false))
    684      continue
    685    endif
    686 
    687    call add(comm_msgs, msg)
    688  endfor
    689 
    690  let curwinid = win_getid()
    691  call win_gotoid(s:gdbwin)
    692 
    693  " Add the output above the current prompt.
    694  for line in lines
    695    " Nvim supports multi-line input in prompt-buffer, so the prompt line is
    696    " not always the last line.
    697    call append(line("':") - 1, line)
    698  endfor
    699  if !empty(lines)
    700    set modified
    701  endif
    702 
    703  call win_gotoid(curwinid)
    704  call s:CommOutput(a:job_id, comm_msgs, a:event)
    705 endfunc
    706 
    707 " Decode a message from gdb.  "quotedText" starts with a ", return the text up
    708 " to the next unescaped ", unescaping characters:
    709 " - remove line breaks (unless "literal" is v:true)
    710 " - change \" to "
    711 " - change \\t to \t (unless "literal" is v:true)
    712 " - change \0xhh to \xhh (disabled for now)
    713 " - change \ooo to octal
    714 " - change \\ to \
    715 func s:DecodeMessage(quotedText, literal)
    716  if a:quotedText[0] != '"'
    717    call s:Echoerr($'DecodeMessage(): missing quote in {a:quotedText}')
    718    return
    719  endif
    720  let msg = a:quotedText
    721        \ ->substitute('^"\|[^\\]\zs".*', '', 'g')
    722        \ ->substitute('\\"', '"', 'g')
    723        "\ multi-byte characters arrive in octal form
    724        "\ NULL-values must be kept encoded as those break the string otherwise
    725        \ ->substitute('\\000', s:NullRepl, 'g')
    726        \ ->substitute('\\\o\o\o', {-> eval('"' .. submatch(0) .. '"')}, 'g')
    727        "\ Note: GDB docs also mention hex encodings - the translations below work
    728        "\       but we keep them out for performance-reasons until we actually see
    729        "\       those in mi-returns
    730        "\ \ ->substitute('\\0x\(\x\x\)', {-> eval('"\x' .. submatch(1) .. '"')}, 'g')
    731        "\ \ ->substitute('\\0x00', s:NullRepl, 'g')
    732        \ ->substitute('\\\\', '\', 'g')
    733        \ ->substitute(s:NullRepl, '\\000', 'g')
    734  if !a:literal
    735    return msg
    736        \ ->substitute('\\t', "\t", 'g')
    737        \ ->substitute('\\n', '', 'g')
    738  else
    739    return msg
    740  endif
    741 endfunc
    742 const s:NullRepl = 'XXXNULLXXX'
    743 
    744 " Extract the "name" value from a gdb message with fullname="name".
    745 func s:GetFullname(msg)
    746  if a:msg !~ 'fullname'
    747    return ''
    748  endif
    749  let name = s:DecodeMessage(substitute(a:msg, '.*fullname=', '', ''), v:true)
    750  if has('win32') && name =~ ':\\\\'
    751    " sometimes the name arrives double-escaped
    752    let name = substitute(name, '\\\\', '\\', 'g')
    753  endif
    754  return name
    755 endfunc
    756 
    757 " Extract the "addr" value from a gdb message with addr="0x0001234".
    758 func s:GetAsmAddr(msg)
    759  if a:msg !~ 'addr='
    760    return ''
    761  endif
    762  let addr = s:DecodeMessage(substitute(a:msg, '.*addr=', '', ''), v:false)
    763  return addr
    764 endfunc
    765 
    766 func s:EndTermDebug(job_id, exit_code, event)
    767  let s:gdb_running = v:false
    768  if s:starting
    769    return
    770  endif
    771 
    772  if exists('#User#TermdebugStopPre')
    773    doauto <nomodeline> User TermdebugStopPre
    774  endif
    775 
    776  unlet s:gdbwin
    777  call s:EndDebugCommon()
    778 endfunc
    779 
    780 func s:EndDebugCommon()
    781  let curwinid = win_getid()
    782 
    783  if exists('s:ptybufnr') && s:ptybufnr
    784    exe $'bwipe! {s:ptybufnr}'
    785  endif
    786  if s:asmbufnr > 0 && bufexists(s:asmbufnr)
    787    exe $'bwipe! {s:asmbufnr}'
    788  endif
    789  if s:varbufnr > 0 && bufexists(s:varbufnr)
    790    exe $'bwipe! {s:varbufnr}'
    791  endif
    792  let s:running = v:false
    793 
    794  " Restore 'signcolumn' in all buffers for which it was set.
    795  call win_gotoid(s:sourcewin)
    796  let was_buf = bufnr()
    797  for bufnr in s:signcolumn_buflist
    798    if bufexists(bufnr)
    799      exe $":{bufnr}buf"
    800      if exists('b:save_signcolumn')
    801        let &signcolumn = b:save_signcolumn
    802        unlet b:save_signcolumn
    803      endif
    804    endif
    805  endfor
    806  if bufexists(was_buf)
    807    exe $":{was_buf}buf"
    808  endif
    809 
    810  call s:DeleteCommands()
    811 
    812  call win_gotoid(curwinid)
    813 
    814  if s:saved_columns > 0
    815    let &columns = s:saved_columns
    816  endif
    817 
    818  if exists('#User#TermdebugStopPost')
    819    doauto <nomodeline> User TermdebugStopPost
    820  endif
    821 
    822  au! TermDebug
    823 endfunc
    824 
    825 func s:EndPromptDebug(job_id, exit_code, event)
    826  if exists('#User#TermdebugStopPre')
    827    doauto <nomodeline> User TermdebugStopPre
    828  endif
    829 
    830  if bufexists(s:promptbuf)
    831    exe $'bwipe! {s:promptbuf}'
    832  endif
    833 
    834  call s:EndDebugCommon()
    835  unlet s:gdbwin
    836  "call ch_log("Returning from EndPromptDebug()")
    837 endfunc
    838 
    839 " - CommOutput: &"disassemble $pc\n"
    840 " - CommOutput: ~"Dump of assembler code for function main(int, char**):\n"
    841 " - CommOutput: ~"   0x0000555556466f69 <+0>:\tpush   rbp\n"
    842 " ...
    843 " - CommOutput: ~"   0x0000555556467cd0:\tpop    rbp\n"
    844 " - CommOutput: ~"   0x0000555556467cd1:\tret    \n"
    845 " - CommOutput: ~"End of assembler dump.\n"
    846 " - CommOutput: ^done
    847 
    848 " - CommOutput: &"disassemble $pc\n"
    849 " - CommOutput: &"No function contains specified address.\n"
    850 " - CommOutput: ^error,msg="No function contains specified address."
    851 func s:HandleDisasmMsg(msg)
    852  if a:msg =~ '^\^done'
    853    let curwinid = win_getid()
    854    if win_gotoid(s:asmwin)
    855      silent! %delete _
    856      call setline(1, s:asm_lines)
    857      set nomodified
    858      set filetype=asm
    859 
    860      let lnum = search($'^{s:asm_addr}')
    861      if lnum != 0
    862        call sign_unplace('TermDebug', #{id: s:asm_id})
    863        call sign_place(s:asm_id, 'TermDebug', 'debugPC', '%', #{lnum: lnum})
    864      endif
    865 
    866      call win_gotoid(curwinid)
    867    endif
    868 
    869    let s:parsing_disasm_msg = 0
    870    let s:asm_lines = []
    871  elseif a:msg =~ '^\^error,msg='
    872    if s:parsing_disasm_msg == 1
    873      " Disassemble call ran into an error. This can happen when gdb can't
    874      " find the function frame address, so let's try to disassemble starting
    875      " at current PC
    876      call s:SendCommand('disassemble $pc,+100')
    877    endif
    878    let s:parsing_disasm_msg = 0
    879  elseif a:msg =~ '^&"disassemble \$pc'
    880    if a:msg =~ '+100'
    881      " This is our second disasm attempt
    882      let s:parsing_disasm_msg = 2
    883    endif
    884  elseif a:msg !~ '^&"disassemble'
    885    let value = substitute(a:msg, '^\~\"[ ]*', '', '')
    886    let value = substitute(value, '^=>[ ]*', '', '')
    887    " Nvim already trims the final "\r" in s:CommOutput()
    888    " let value = substitute(value, '\\n\"\r$', '', '')
    889    let value = substitute(value, '\\n\"$', '', '')
    890    let value = substitute(value, '\r', '', '')
    891    let value = substitute(value, '\\t', ' ', 'g')
    892 
    893    if value != '' || !empty(s:asm_lines)
    894      call add(s:asm_lines, value)
    895    endif
    896  endif
    897 endfunc
    898 
    899 func s:ParseVarinfo(varinfo)
    900  let dict = {}
    901  let nameIdx = matchstrpos(a:varinfo, '{name="\([^"]*\)"')
    902  let dict['name'] = a:varinfo[nameIdx[1] + 7 : nameIdx[2] - 2]
    903  let typeIdx = matchstrpos(a:varinfo, ',type="\([^"]*\)"')
    904  " 'type' maybe is a url-like string,
    905  " try to shorten it and show only the /tail
    906  let dict['type'] = (a:varinfo[typeIdx[1] + 7 : typeIdx[2] - 2])->fnamemodify(':t')
    907  let valueIdx = matchstrpos(a:varinfo, ',value="\(.*\)"}')
    908  if valueIdx[1] == -1
    909    let dict['value'] = 'Complex value'
    910  else
    911    let dict['value'] = a:varinfo[valueIdx[1] + 8 : valueIdx[2] - 3]
    912  endif
    913  return dict
    914 endfunc
    915 
    916 func s:HandleVariablesMsg(msg)
    917  let curwinid = win_getid()
    918  if win_gotoid(s:varwin)
    919 
    920    silent! %delete _
    921    let spaceBuffer = 20
    922    let spaces = repeat(' ', 16)
    923    call setline(1, $'Type{spaces}Name{spaces}Value')
    924    let cnt = 1
    925    let capture = '{name=".\{-}",\%(arg=".\{-}",\)\{0,1\}type=".\{-}"\%(,value=".\{-}"\)\{0,1\}}'
    926    let varinfo = matchstr(a:msg, capture, 0, cnt)
    927    while varinfo != ''
    928      let vardict = s:ParseVarinfo(varinfo)
    929      call setline(cnt + 1, vardict['type'] .
    930            \ repeat(' ', max([20 - len(vardict['type']), 1])) .
    931            \ vardict['name'] .
    932            \ repeat(' ', max([20 - len(vardict['name']), 1])) .
    933            \ vardict['value'])
    934      let cnt += 1
    935      let varinfo = matchstr(a:msg, capture, 0, cnt)
    936    endwhile
    937  endif
    938  call win_gotoid(curwinid)
    939 endfunc
    940 
    941 func s:CommOutput(job_id, msgs, event)
    942  for msg in a:msgs
    943    " Nvim job lines are split on "\n", so trim a suffixed CR.
    944    if msg[-1:] == "\r"
    945      let msg = msg[:-2]
    946    endif
    947 
    948    if s:parsing_disasm_msg
    949      call s:HandleDisasmMsg(msg)
    950    elseif msg != ''
    951      if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)'
    952        call s:HandleCursor(msg)
    953      elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,'
    954        call s:HandleNewBreakpoint(msg, 0)
    955      elseif msg =~ '^=breakpoint-modified,'
    956        call s:HandleNewBreakpoint(msg, 1)
    957      elseif msg =~ '^=breakpoint-deleted,'
    958        call s:HandleBreakpointDelete(msg)
    959      elseif msg =~ '^=thread-group-started'
    960        call s:HandleProgramRun(msg)
    961      elseif msg =~ '^\^done,value='
    962        call s:HandleEvaluate(msg)
    963      elseif msg =~ '^\^error,msg='
    964        call s:HandleError(msg)
    965      elseif msg =~ '^&"disassemble'
    966        let s:parsing_disasm_msg = 1
    967        let s:asm_lines = []
    968        call s:HandleDisasmMsg(msg)
    969      elseif msg =~ '^\^done,variables='
    970        call s:HandleVariablesMsg(msg)
    971      endif
    972    endif
    973  endfor
    974 endfunc
    975 
    976 func s:GotoProgram()
    977  if has('win32')
    978    if executable('powershell')
    979      call system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', s:pid))
    980    endif
    981  else
    982    call win_gotoid(s:ptywin)
    983  endif
    984 endfunc
    985 
    986 " Install commands in the current window to control the debugger.
    987 func s:InstallCommands()
    988  let save_cpo = &cpo
    989  set cpo&vim
    990 
    991  command -nargs=? Break call s:SetBreakpoint(<q-args>)
    992  command -nargs=? Tbreak call s:SetBreakpoint(<q-args>, v:true)
    993  command Clear call s:ClearBreakpoint()
    994  command Step call s:SendResumingCommand('-exec-step')
    995  command Over call s:SendResumingCommand('-exec-next')
    996  command -nargs=? Until call s:Until(<q-args>)
    997  command Finish call s:SendResumingCommand('-exec-finish')
    998  command -nargs=* Run call s:Run(<q-args>)
    999  command -nargs=* Arguments call s:SendResumingCommand('-exec-arguments ' . <q-args>)
   1000 
   1001  if s:way == 'prompt'
   1002    command Stop call s:PromptInterrupt()
   1003    command Continue call s:SendCommand('continue')
   1004  else
   1005    command Stop call s:SendCommand('-exec-interrupt')
   1006    " using -exec-continue results in CTRL-C in the gdb window not working,
   1007    " communicating via commbuf (= use of SendCommand) has the same result
   1008    "command Continue  call s:SendCommand('-exec-continue')
   1009    command Continue call chansend(s:gdb_job_id, "continue\r")
   1010  endif
   1011 
   1012  command -nargs=* Frame call s:Frame(<q-args>)
   1013  command -count=1 Up call s:Up(<count>)
   1014  command -count=1 Down call s:Down(<count>)
   1015 
   1016  command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
   1017  command Gdb call win_gotoid(s:gdbwin)
   1018  command Program call s:GotoProgram()
   1019  command Source call s:GotoSourcewinOrCreateIt()
   1020  command Asm call s:GotoAsmwinOrCreateIt()
   1021  command Var call s:GotoVariableswinOrCreateIt()
   1022  command Winbar call s:InstallWinbar(1)
   1023 
   1024  let map = 1
   1025  if exists('g:termdebug_config')
   1026    let map = get(g:termdebug_config, 'map_K', 1)
   1027  elseif exists('g:termdebug_map_K')
   1028    let map = g:termdebug_map_K
   1029  endif
   1030  if map
   1031    let s:saved_K_map = maparg('K', 'n', 0, 1)
   1032    if !empty(s:saved_K_map) && !s:saved_K_map.buffer || empty(s:saved_K_map)
   1033      nnoremap K :Evaluate<CR>
   1034    endif
   1035  endif
   1036 
   1037  let map = 1
   1038  if exists('g:termdebug_config')
   1039    let map = get(g:termdebug_config, 'map_plus', 1)
   1040  endif
   1041  if map
   1042    let s:saved_plus_map = maparg('+', 'n', 0, 1)
   1043    if !empty(s:saved_plus_map) && !s:saved_plus_map.buffer || empty(s:saved_plus_map)
   1044      nnoremap <expr> + $'<Cmd>{v:count1}Up<CR>'
   1045    endif
   1046  endif
   1047 
   1048  let map = 1
   1049  if exists('g:termdebug_config')
   1050    let map = get(g:termdebug_config, 'map_minus', 1)
   1051  endif
   1052  if map
   1053    let s:saved_minus_map = maparg('-', 'n', 0, 1)
   1054    if !empty(s:saved_minus_map) && !s:saved_minus_map.buffer || empty(s:saved_minus_map)
   1055      nnoremap <expr> - $'<Cmd>{v:count1}Down<CR>'
   1056    endif
   1057  endif
   1058 
   1059 
   1060  if has('menu') && &mouse != ''
   1061    call s:InstallWinbar(0)
   1062 
   1063    let popup = 1
   1064    if exists('g:termdebug_config')
   1065      let popup = get(g:termdebug_config, 'popup', 1)
   1066    elseif exists('g:termdebug_popup')
   1067      let popup = g:termdebug_popup
   1068    endif
   1069    if popup
   1070      let s:saved_mousemodel = &mousemodel
   1071      let &mousemodel = 'popup_setpos'
   1072      an 1.200 PopUp.-SEP3-     <Nop>
   1073      an 1.210 PopUp.Set\ breakpoint    :Break<CR>
   1074      an 1.220 PopUp.Clear\ breakpoint  :Clear<CR>
   1075      an 1.230 PopUp.Run\ until         :Until<CR>
   1076      an 1.240 PopUp.Evaluate           :Evaluate<CR>
   1077    endif
   1078  endif
   1079 
   1080  let &cpo = save_cpo
   1081 endfunc
   1082 
   1083 " let s:winbar_winids = []
   1084 
   1085 " Install the window toolbar in the current window.
   1086 func s:InstallWinbar(force)
   1087  " if has('menu') && &mouse != ''
   1088  "   nnoremenu WinBar.Step   :Step<CR>
   1089  "   nnoremenu WinBar.Next   :Over<CR>
   1090  "   nnoremenu WinBar.Finish :Finish<CR>
   1091  "   nnoremenu WinBar.Cont   :Continue<CR>
   1092  "   nnoremenu WinBar.Stop   :Stop<CR>
   1093  "   nnoremenu WinBar.Eval   :Evaluate<CR>
   1094  "   call add(s:winbar_winids, win_getid())
   1095  " endif
   1096 endfunc
   1097 
   1098 " Delete installed debugger commands in the current window.
   1099 func s:DeleteCommands()
   1100  delcommand Break
   1101  delcommand Tbreak
   1102  delcommand Clear
   1103  delcommand Step
   1104  delcommand Over
   1105  delcommand Until
   1106  delcommand Finish
   1107  delcommand Run
   1108  delcommand Arguments
   1109  delcommand Stop
   1110  delcommand Continue
   1111  delcommand Frame
   1112  delcommand Up
   1113  delcommand Down
   1114  delcommand Evaluate
   1115  delcommand Gdb
   1116  delcommand Program
   1117  delcommand Source
   1118  delcommand Asm
   1119  delcommand Var
   1120  delcommand Winbar
   1121 
   1122  if exists('s:saved_K_map')
   1123    if !empty(s:saved_K_map) && !s:saved_K_map.buffer
   1124      nunmap K
   1125      call mapset(s:saved_K_map)
   1126    elseif empty(s:saved_K_map)
   1127      nunmap K
   1128    endif
   1129    unlet s:saved_K_map
   1130  endif
   1131  if exists('s:saved_plus_map')
   1132    if !empty(s:saved_plus_map) && !s:saved_plus_map.buffer
   1133      nunmap +
   1134      call mapset(s:saved_plus_map)
   1135    elseif empty(s:saved_plus_map)
   1136      nunmap +
   1137    endif
   1138    unlet s:saved_plus_map
   1139  endif
   1140  if exists('s:saved_minus_map')
   1141    if !empty(s:saved_minus_map) && !s:saved_minus_map.buffer
   1142      nunmap -
   1143      call mapset(s:saved_minus_map)
   1144    elseif empty(s:saved_minus_map)
   1145      nunmap -
   1146    endif
   1147    unlet s:saved_minus_map
   1148  endif
   1149 
   1150  if has('menu')
   1151    " Remove the WinBar entries from all windows where it was added.
   1152    " let curwinid = win_getid()
   1153    " for winid in s:winbar_winids
   1154    "   if win_gotoid(winid)
   1155    "     aunmenu WinBar.Step
   1156    "     aunmenu WinBar.Next
   1157    "     aunmenu WinBar.Finish
   1158    "     aunmenu WinBar.Cont
   1159    "     aunmenu WinBar.Stop
   1160    "     aunmenu WinBar.Eval
   1161    "   endif
   1162    " endfor
   1163    " call win_gotoid(curwinid)
   1164    " let s:winbar_winids = []
   1165 
   1166    if exists('s:saved_mousemodel')
   1167      let &mousemodel = s:saved_mousemodel
   1168      unlet s:saved_mousemodel
   1169      aunmenu PopUp.-SEP3-
   1170      aunmenu PopUp.Set\ breakpoint
   1171      aunmenu PopUp.Clear\ breakpoint
   1172      aunmenu PopUp.Run\ until
   1173      aunmenu PopUp.Evaluate
   1174    endif
   1175  endif
   1176 
   1177  call sign_unplace('TermDebug')
   1178  unlet s:breakpoints
   1179  unlet s:breakpoint_locations
   1180 
   1181  call sign_undefine('debugPC')
   1182  call sign_undefine(s:BreakpointSigns->map("'debugBreakpoint' .. v:val"))
   1183  let s:BreakpointSigns = []
   1184 endfunc
   1185 
   1186 func s:QuoteArg(x)
   1187  " Find all the occurrences of " and \ and escape them and double quote
   1188  " the resulting string.
   1189  return printf('"%s"', a:x->substitute('[\\"]', '\\&', 'g'))
   1190 endfunc
   1191 
   1192 " :Until - Execute until past a specified position or current line
   1193 func s:Until(at)
   1194  if s:stopped
   1195    " reset s:stopped here, it may take a bit of time before we get a response
   1196    let s:stopped = v:false
   1197    " call ch_log('assume that program is running after this command')
   1198    " Use the fname:lnum format
   1199    let at = empty(a:at) ? s:QuoteArg($"{expand('%:p')}:{line('.')}") : a:at
   1200    call s:SendCommand($'-exec-until {at}')
   1201  " else
   1202    " call ch_log('dropping command, program is running: exec-until')
   1203  endif
   1204 endfunc
   1205 
   1206 " :Break - Set a breakpoint at the cursor position.
   1207 func s:SetBreakpoint(at, tbreak=v:false)
   1208  " Setting a breakpoint may not work while the program is running.
   1209  " Interrupt to make it work.
   1210  let do_continue = 0
   1211  if !s:stopped
   1212    let do_continue = 1
   1213    Stop
   1214    sleep 10m
   1215  endif
   1216 
   1217  " Use the fname:lnum format, older gdb can't handle --source.
   1218  let at = empty(a:at) ? s:QuoteArg($"{expand('%:p')}:{line('.')}") : a:at
   1219  if a:tbreak
   1220    let cmd = $'-break-insert -t {at}'
   1221  else
   1222    let cmd = $'-break-insert {at}'
   1223  endif
   1224  call s:SendCommand(cmd)
   1225  if do_continue
   1226    Continue
   1227  endif
   1228 endfunc
   1229 
   1230 " :Clear - Delete a breakpoint at the cursor position.
   1231 func s:ClearBreakpoint()
   1232  let fname = fnameescape(expand('%:p'))
   1233  let lnum = line('.')
   1234  let bploc = printf('%s:%d', fname, lnum)
   1235  if has_key(s:breakpoint_locations, bploc)
   1236    let idx = 0
   1237    let nr = 0
   1238    for id in s:breakpoint_locations[bploc]
   1239      if has_key(s:breakpoints, id)
   1240        " Assume this always works, the reply is simply "^done".
   1241        call s:SendCommand($'-break-delete {id}')
   1242        for subid in keys(s:breakpoints[id])
   1243          call sign_unplace('TermDebug',
   1244                \ #{id: s:Breakpoint2SignNumber(id, subid)})
   1245        endfor
   1246        unlet s:breakpoints[id]
   1247        unlet s:breakpoint_locations[bploc][idx]
   1248        let nr = id
   1249        break
   1250      else
   1251        let idx += 1
   1252      endif
   1253    endfor
   1254    if nr != 0
   1255      if empty(s:breakpoint_locations[bploc])
   1256        unlet s:breakpoint_locations[bploc]
   1257      endif
   1258      echomsg $'Breakpoint {nr} cleared from line {lnum}.'
   1259    else
   1260      call s:Echoerr($'Internal error trying to remove breakpoint at line {lnum}!')
   1261    endif
   1262  else
   1263    echomsg $'No breakpoint to remove at line {lnum}.'
   1264  endif
   1265 endfunc
   1266 
   1267 func s:Run(args)
   1268  if a:args != ''
   1269    call s:SendResumingCommand($'-exec-arguments {a:args}')
   1270  endif
   1271  call s:SendResumingCommand('-exec-run')
   1272 endfunc
   1273 
   1274 " :Frame - go to a specific frame in the stack
   1275 func s:Frame(arg)
   1276  " Note: we explicit do not use mi's command
   1277  " call s:SendCommand('-stack-select-frame "' . a:arg .'"')
   1278  " as we only get a "done" mi response and would have to open the file
   1279  " 'manually' - using cli command "frame" provides us with the mi response
   1280  " already parsed and allows for more formats
   1281  if a:arg =~ '^\d\+$' || a:arg == ''
   1282    " specify frame by number
   1283    call s:SendCommand($'-interpreter-exec mi "frame {a:arg}"')
   1284  elseif a:arg =~ '^0x[0-9a-fA-F]\+$'
   1285    " specify frame by stack address
   1286    call s:SendCommand($'-interpreter-exec mi "frame address {a:arg}"')
   1287  else
   1288    " specify frame by function name
   1289    call s:SendCommand($'-interpreter-exec mi "frame function {a:arg}"')
   1290  endif
   1291 endfunc
   1292 
   1293 " :Up - go a:count frames in the stack "higher"
   1294 func s:Up(count)
   1295  " the 'correct' one would be -stack-select-frame N, but we don't know N
   1296  call s:SendCommand($'-interpreter-exec console "up {a:count}"')
   1297 endfunc
   1298 
   1299 " :Down - go a:count frames in the stack "below"
   1300 func s:Down(count)
   1301  " the 'correct' one would be -stack-select-frame N, but we don't know N
   1302  call s:SendCommand($'-interpreter-exec console "down {a:count}"')
   1303 endfunc
   1304 
   1305 func s:SendEval(expr)
   1306  " check for "likely" boolean expressions, in which case we take it as lhs
   1307  if a:expr =~ "[=!<>]="
   1308    let exprLHS = a:expr
   1309  else
   1310    " remove text that is likely an assignment
   1311    let exprLHS = substitute(a:expr, ' *=.*', '', '')
   1312  endif
   1313 
   1314  " encoding expression to prevent bad errors
   1315  let expr_escaped = a:expr
   1316        \ ->substitute('\\', '\\\\', 'g')
   1317        \ ->substitute('"', '\\"', 'g')
   1318  call s:SendCommand($'-data-evaluate-expression "{expr_escaped}"')
   1319  let s:evalexpr = exprLHS
   1320 endfunc
   1321 
   1322 " :Evaluate - evaluate what is specified / under the cursor
   1323 func s:Evaluate(range, arg)
   1324  if s:eval_float_win_id > 0 && nvim_win_is_valid(s:eval_float_win_id)
   1325        \ && a:range == 0 && empty(a:arg)
   1326    call nvim_set_current_win(s:eval_float_win_id)
   1327    return
   1328  endif
   1329  let expr = s:GetEvaluationExpression(a:range, a:arg)
   1330  let s:evalFromBalloonExpr = v:true
   1331  let s:evalFromBalloonExprResult = ''
   1332  let s:ignoreEvalError = v:false
   1333  call s:SendEval(expr)
   1334 endfunc
   1335 
   1336 " get what is specified / under the cursor
   1337 func s:GetEvaluationExpression(range, arg)
   1338  if a:arg != ''
   1339    " user supplied evaluation
   1340    let expr = s:CleanupExpr(a:arg)
   1341    " DSW: replace "likely copy + paste" assignment
   1342    let expr = substitute(expr, '"\([^"]*\)": *', '\1=', 'g')
   1343  elseif a:range == 2
   1344    let pos = getcurpos()
   1345    let reg = getreg('v', 1, 1)
   1346    let regt = getregtype('v')
   1347    normal! gv"vy
   1348    let expr = s:CleanupExpr(@v)
   1349    call setpos('.', pos)
   1350    call setreg('v', reg, regt)
   1351    let s:evalFromBalloonExpr = v:true
   1352  else
   1353    " no evaluation provided: get from C-expression under cursor
   1354    " TODO: allow filetype specific lookup #9057
   1355    let expr = expand('<cexpr>')
   1356    let s:evalFromBalloonExpr = v:true
   1357  endif
   1358  return expr
   1359 endfunc
   1360 
   1361 " clean up expression that may get in because of range
   1362 " (newlines and surrounding whitespace)
   1363 " As it can also be specified via ex-command for assignments this function
   1364 " may not change the "content" parts (like replacing contained spaces)
   1365 func s:CleanupExpr(expr)
   1366  " replace all embedded newlines/tabs/...
   1367  let expr = substitute(a:expr, '\_s', ' ', 'g')
   1368 
   1369  if &filetype ==# 'cobol'
   1370    " extra cleanup for COBOL:
   1371    " - a semicolon nmay be used instead of a space
   1372    " - a trailing comma or period is ignored as it commonly separates/ends
   1373    "   multiple expr
   1374    let expr = substitute(expr, ';', ' ', 'g')
   1375    let expr = substitute(expr, '[,.]\+ *$', '', '')
   1376  endif
   1377 
   1378  " get rid of leading and trailing spaces
   1379  let expr = substitute(expr, '^ *', '', '')
   1380  let expr = substitute(expr, ' *$', '', '')
   1381  return expr
   1382 endfunc
   1383 
   1384 let s:ignoreEvalError = v:false
   1385 let s:evalFromBalloonExpr = v:false
   1386 let s:evalFromBalloonExprResult = ''
   1387 
   1388 let s:eval_float_win_id = -1
   1389 
   1390 " Handle the result of data-evaluate-expression
   1391 func s:HandleEvaluate(msg)
   1392  let value = a:msg
   1393        \ ->substitute('.*value="\(.*\)"', '\1', '')
   1394        \ ->substitute('\\"', '"', 'g')
   1395        \ ->substitute('\\\\', '\\', 'g')
   1396        "\ multi-byte characters arrive in octal form, replace everything but NULL values
   1397        \ ->substitute('\\000', s:NullRepl, 'g')
   1398        \ ->substitute('\\\o\o\o', {-> eval('"' .. submatch(0) .. '"')}, 'g')
   1399        "\ Note: GDB docs also mention hex encodings - the translations below work
   1400        "\       but we keep them out for performance-reasons until we actually see
   1401        "\       those in mi-returns
   1402        "\ ->substitute('\\0x00', s:NullRep, 'g')
   1403        "\ ->substitute('\\0x\(\x\x\)', {-> eval('"\x' .. submatch(1) .. '"')}, 'g')
   1404        \ ->substitute(s:NullRepl, '\\000', 'g')
   1405        \ ->substitute('', '\1', '')
   1406  if s:evalFromBalloonExpr
   1407    if s:evalFromBalloonExprResult == ''
   1408      let s:evalFromBalloonExprResult = $'{s:evalexpr}: {value}'
   1409    else
   1410      let s:evalFromBalloonExprResult ..= $' = {value}'
   1411    endif
   1412    " NEOVIM:
   1413    " - Result pretty-printing is not implemented. Vim prettifies the result
   1414    "   with balloon_split(), which is not ported to nvim.
   1415    " - Manually implement window focusing. Sometimes the result of pointer
   1416    "   evaluation arrives in two separate messages, one for the address
   1417    "   itself and the other for the value in that address. So with the stock
   1418    "   focus option, the second message will focus the window containing the
   1419    "   first message.
   1420    let s:eval_float_win_id = luaeval('select(2, vim.lsp.util.open_floating_preview(_A))', [s:evalFromBalloonExprResult])
   1421  else
   1422    echomsg $'"{s:evalexpr}": {value}'
   1423  endif
   1424 
   1425  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
   1426    " Looks like a pointer, also display what it points to.
   1427    let s:ignoreEvalError = v:true
   1428    call s:SendEval($'*{s:evalexpr}')
   1429  endif
   1430 endfunc
   1431 
   1432 " Handle an error.
   1433 func s:HandleError(msg)
   1434  if s:ignoreEvalError
   1435    " Result of s:SendEval() failed, ignore.
   1436    let s:ignoreEvalError = v:false
   1437    let s:evalFromBalloonExpr = v:false
   1438    return
   1439  endif
   1440  let msgVal = substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
   1441  call s:Echoerr(substitute(msgVal, '\\"', '"', 'g'))
   1442 endfunc
   1443 
   1444 func s:GotoSourcewinOrCreateIt()
   1445  if !win_gotoid(s:sourcewin)
   1446    new
   1447    let s:sourcewin = win_getid()
   1448    call s:InstallWinbar(0)
   1449  endif
   1450 endfunc
   1451 
   1452 func s:GetDisasmWindow()
   1453  if exists('g:termdebug_config')
   1454    return get(g:termdebug_config, 'disasm_window', 0)
   1455  endif
   1456  if exists('g:termdebug_disasm_window')
   1457    return g:termdebug_disasm_window
   1458  endif
   1459  return 0
   1460 endfunc
   1461 
   1462 func s:GetDisasmWindowHeight()
   1463  if exists('g:termdebug_config')
   1464    return get(g:termdebug_config, 'disasm_window_height', 0)
   1465  endif
   1466  if exists('g:termdebug_disasm_window') && g:termdebug_disasm_window > 1
   1467    return g:termdebug_disasm_window
   1468  endif
   1469  return 0
   1470 endfunc
   1471 
   1472 func s:GotoAsmwinOrCreateIt()
   1473  if !win_gotoid(s:asmwin)
   1474    let mdf = ''
   1475    if win_gotoid(s:sourcewin)
   1476      " 60 is approx spaceBuffer * 3
   1477      if winwidth(0) > (78 + 60)
   1478        let mdf = 'vert'
   1479        exe $'{mdf} :60new'
   1480      else
   1481        exe 'rightbelow new'
   1482      endif
   1483    else
   1484      exe 'new'
   1485    endif
   1486 
   1487    let s:asmwin = win_getid()
   1488 
   1489    setlocal nowrap
   1490    setlocal number
   1491    setlocal noswapfile
   1492    setlocal buftype=nofile
   1493    setlocal bufhidden=wipe
   1494    setlocal signcolumn=no
   1495    setlocal modifiable
   1496 
   1497    if s:asmbufnr > 0 && bufexists(s:asmbufnr)
   1498      exe $'buffer {s:asmbufnr}'
   1499    elseif empty(glob('Termdebug-asm-listing'))
   1500      silent file Termdebug-asm-listing
   1501      let s:asmbufnr = bufnr('Termdebug-asm-listing')
   1502    else
   1503      call s:Echoerr("You have a file/folder named 'Termdebug-asm-listing'.
   1504          \ Please exit and rename it because Termdebug may not work as expected.")
   1505    endif
   1506 
   1507    if mdf != 'vert' && s:GetDisasmWindowHeight() > 0
   1508      exe $'resize {s:GetDisasmWindowHeight()}'
   1509    endif
   1510  endif
   1511 
   1512  if s:asm_addr != ''
   1513    let lnum = search($'^{s:asm_addr}')
   1514    if lnum == 0
   1515      if s:stopped
   1516        call s:SendCommand('disassemble $pc')
   1517      endif
   1518    else
   1519      call sign_unplace('TermDebug', #{id: s:asm_id})
   1520      call sign_place(s:asm_id, 'TermDebug', 'debugPC', '%', #{lnum: lnum})
   1521    endif
   1522  endif
   1523 endfunc
   1524 
   1525 func s:GetVariablesWindow()
   1526  if exists('g:termdebug_config')
   1527    return get(g:termdebug_config, 'variables_window', 0)
   1528  endif
   1529  if exists('g:termdebug_disasm_window')
   1530    return g:termdebug_variables_window
   1531  endif
   1532  return 0
   1533 endfunc
   1534 
   1535 func s:GetVariablesWindowHeight()
   1536  if exists('g:termdebug_config')
   1537    return get(g:termdebug_config, 'variables_window_height', 0)
   1538  endif
   1539  if exists('g:termdebug_variables_window') && g:termdebug_variables_window > 1
   1540    return g:termdebug_variables_window
   1541  endif
   1542  return 0
   1543 endfunc
   1544 
   1545 func s:GotoVariableswinOrCreateIt()
   1546  if !win_gotoid(s:varwin)
   1547    let mdf = ''
   1548    if win_gotoid(s:sourcewin)
   1549      " 60 is approx spaceBuffer * 3
   1550      if winwidth(0) > (78 + 60)
   1551        let mdf = 'vert'
   1552        exe $'{mdf} :60new'
   1553      else
   1554        exe 'rightbelow new'
   1555      endif
   1556    else
   1557      exe 'new'
   1558    endif
   1559 
   1560    let s:varwin = win_getid()
   1561 
   1562    setlocal nowrap
   1563    setlocal noswapfile
   1564    setlocal buftype=nofile
   1565    setlocal bufhidden=wipe
   1566    setlocal signcolumn=no
   1567    setlocal modifiable
   1568 
   1569    if s:varbufnr > 0 && bufexists(s:varbufnr)
   1570      exe $'buffer {s:varbufnr}'
   1571    elseif empty(glob('Termdebug-variables-listing'))
   1572      silent file Termdebug-variables-listing
   1573      let s:varbufnr = bufnr('Termdebug-variables-listing')
   1574    else
   1575      call s:Echoerr("You have a file/folder named 'Termdebug-variables-listing'.
   1576          \ Please exit and rename it because Termdebug may not work as expected.")
   1577    endif
   1578 
   1579    if mdf != 'vert' && s:GetVariablesWindowHeight() > 0
   1580      exe $'resize {s:GetVariablesWindowHeight()}'
   1581    endif
   1582  endif
   1583 
   1584  if s:running
   1585    call s:SendCommand('-stack-list-variables 2')
   1586  endif
   1587 endfunc
   1588 
   1589 " Handle stopping and running message from gdb.
   1590 " Will update the sign that shows the current position.
   1591 func s:HandleCursor(msg)
   1592  let wid = win_getid()
   1593 
   1594  if a:msg =~ '^\*stopped'
   1595    "call ch_log('program stopped')
   1596    let s:stopped = v:true
   1597    if a:msg =~ '^\*stopped,reason="exited-normally"'
   1598      let s:running = v:false
   1599    endif
   1600  elseif a:msg =~ '^\*running'
   1601    "call ch_log('program running')
   1602    let s:stopped = v:false
   1603    let s:running = v:true
   1604  endif
   1605 
   1606  if a:msg =~ 'fullname='
   1607    let fname = s:GetFullname(a:msg)
   1608  else
   1609    let fname = ''
   1610  endif
   1611 
   1612  if a:msg =~ 'addr='
   1613    let asm_addr = s:GetAsmAddr(a:msg)
   1614    if asm_addr != ''
   1615      let s:asm_addr = asm_addr
   1616 
   1617      let curwinid = win_getid()
   1618      if win_gotoid(s:asmwin)
   1619        let lnum = search($'^{s:asm_addr}')
   1620        if lnum == 0
   1621          call s:SendCommand('disassemble $pc')
   1622        else
   1623          call sign_unplace('TermDebug', #{id: s:asm_id})
   1624          call sign_place(s:asm_id, 'TermDebug', 'debugPC', '%', #{lnum: lnum})
   1625        endif
   1626 
   1627        call win_gotoid(curwinid)
   1628      endif
   1629    endif
   1630  endif
   1631 
   1632  if s:running && s:stopped && bufwinnr('Termdebug-variables-listing') != -1
   1633    call s:SendCommand('-stack-list-variables 2')
   1634  endif
   1635 
   1636  if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
   1637    let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
   1638    if lnum =~ '^[0-9]*$'
   1639      call s:GotoSourcewinOrCreateIt()
   1640      if expand('%:p') != fnamemodify(fname, ':p')
   1641        echomsg $"different fname: '{expand('%:p')}' vs '{fnamemodify(fname, ':p')}'"
   1642        augroup Termdebug
   1643          " Always open a file read-only instead of showing the ATTENTION
   1644          " prompt, since it is unlikely we want to edit the file.
   1645          " The file may be changed but not saved, warn for that.
   1646          au SwapExists * echohl WarningMsg
   1647          \ | echo 'Warning: file is being edited elsewhere'
   1648          \ | echohl None
   1649          \ | let v:swapchoice = 'o'
   1650        augroup END
   1651        if &modified
   1652          " TODO: find existing window
   1653          exe $'split {fnameescape(fname)}'
   1654          let s:sourcewin = win_getid()
   1655          call s:InstallWinbar(0)
   1656        else
   1657          exe $'edit {fnameescape(fname)}'
   1658        endif
   1659        augroup Termdebug
   1660          au! SwapExists
   1661        augroup END
   1662      endif
   1663      exe $":{lnum}"
   1664      normal! zv
   1665      call sign_unplace('TermDebug', #{id: s:pc_id})
   1666      call sign_place(s:pc_id, 'TermDebug', 'debugPC', fname,
   1667            \ #{lnum: lnum, priority: 110})
   1668      if !exists('b:save_signcolumn')
   1669        let b:save_signcolumn = &signcolumn
   1670        call add(s:signcolumn_buflist, bufnr())
   1671      endif
   1672      setlocal signcolumn=yes
   1673    endif
   1674  elseif !s:stopped || fname != ''
   1675    call sign_unplace('TermDebug', #{id: s:pc_id})
   1676  endif
   1677 
   1678  call win_gotoid(wid)
   1679 endfunc
   1680 
   1681 let s:BreakpointSigns = []
   1682 
   1683 func s:CreateBreakpoint(id, subid, enabled)
   1684  let nr = printf('%d.%d', a:id, a:subid)
   1685  if index(s:BreakpointSigns, nr) == -1
   1686    call add(s:BreakpointSigns, nr)
   1687    if a:enabled == "n"
   1688      let hiName = "debugBreakpointDisabled"
   1689    else
   1690      let hiName = "debugBreakpoint"
   1691    endif
   1692    let label = ''
   1693    if exists('g:termdebug_config')
   1694      if has_key(g:termdebug_config, 'signs')
   1695        let label = get(g:termdebug_config.signs, a:id - 1, '')
   1696      endif
   1697      if label == '' && has_key(g:termdebug_config, 'sign')
   1698        let label = g:termdebug_config['sign']
   1699      endif
   1700      if label == '' && has_key(g:termdebug_config, 'sign_decimal')
   1701        let label = printf('%02d', a:id)
   1702        if a:id > 99
   1703          let label = '9+'
   1704        endif
   1705      endif
   1706    endif
   1707    if label == ''
   1708      let label = printf('%02X', a:id)
   1709      if a:id > 255
   1710        let label = 'F+'
   1711      endif
   1712    endif
   1713    call sign_define($'debugBreakpoint{nr}',
   1714          \ #{text: slice(label, 0, 2),
   1715          \ texthl: hiName})
   1716  endif
   1717 endfunc
   1718 
   1719 func! s:SplitMsg(s)
   1720  return split(a:s, '{.\{-}}\zs')
   1721 endfunction
   1722 
   1723 " Handle setting a breakpoint
   1724 " Will update the sign that shows the breakpoint
   1725 func s:HandleNewBreakpoint(msg, modifiedFlag)
   1726  if a:msg !~ 'fullname='
   1727    " a watch or a pending breakpoint does not have a file name
   1728    if a:msg =~ 'pending='
   1729      let nr = substitute(a:msg, '.*number=\"\([0-9.]*\)\".*', '\1', '')
   1730      let target = substitute(a:msg, '.*pending=\"\([^"]*\)\".*', '\1', '')
   1731      echomsg $'Breakpoint {nr} ({target}) pending.'
   1732    endif
   1733    return
   1734  endif
   1735  for msg in s:SplitMsg(a:msg)
   1736    let fname = s:GetFullname(msg)
   1737    if empty(fname)
   1738      continue
   1739    endif
   1740    let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '')
   1741    if empty(nr)
   1742      return
   1743    endif
   1744 
   1745    " If "nr" is 123 it becomes "123.0" and subid is "0".
   1746    " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded.
   1747    let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0')
   1748    let enabled = substitute(msg, '.*enabled="\([yn]\)".*', '\1', '')
   1749    call s:CreateBreakpoint(id, subid, enabled)
   1750 
   1751    if has_key(s:breakpoints, id)
   1752      let entries = s:breakpoints[id]
   1753    else
   1754      let entries = {}
   1755      let s:breakpoints[id] = entries
   1756    endif
   1757    if has_key(entries, subid)
   1758      let entry = entries[subid]
   1759    else
   1760      let entry = {}
   1761      let entries[subid] = entry
   1762    endif
   1763 
   1764    let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '')
   1765    let entry['fname'] = fname
   1766    let entry['lnum'] = lnum
   1767 
   1768    let bploc = printf('%s:%d', fname, lnum)
   1769    if !has_key(s:breakpoint_locations, bploc)
   1770      let s:breakpoint_locations[bploc] = []
   1771    endif
   1772    let s:breakpoint_locations[bploc] += [id]
   1773 
   1774    if bufloaded(fname)
   1775      call s:PlaceSign(id, subid, entry)
   1776      let posMsg = $' at line {lnum}.'
   1777    else
   1778      let posMsg = $' in {fname} at line {lnum}.'
   1779    endif
   1780    if !a:modifiedFlag
   1781      let actionTaken = 'created'
   1782    elseif enabled == 'n'
   1783      let actionTaken = 'disabled'
   1784    else
   1785      let actionTaken = 'enabled'
   1786    endif
   1787    echom $'Breakpoint {nr} {actionTaken}{posMsg}'
   1788  endfor
   1789 endfunc
   1790 
   1791 func s:PlaceSign(id, subid, entry)
   1792  let nr = printf('%d.%d', a:id, a:subid)
   1793  call sign_place(s:Breakpoint2SignNumber(a:id, a:subid), 'TermDebug',
   1794        \ $'debugBreakpoint{nr}', a:entry['fname'],
   1795        \ #{lnum: a:entry['lnum'], priority: 110})
   1796  let a:entry['placed'] = 1
   1797 endfunc
   1798 
   1799 " Handle deleting a breakpoint
   1800 " Will remove the sign that shows the breakpoint
   1801 func s:HandleBreakpointDelete(msg)
   1802  let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
   1803  if empty(id)
   1804    return
   1805  endif
   1806  if has_key(s:breakpoints, id)
   1807    for [subid, entry] in items(s:breakpoints[id])
   1808      if has_key(entry, 'placed')
   1809        call sign_unplace('TermDebug',
   1810              \ #{id: s:Breakpoint2SignNumber(id, subid)})
   1811        unlet entry['placed']
   1812      endif
   1813    endfor
   1814    unlet s:breakpoints[id]
   1815    echomsg $'Breakpoint {id} cleared.'
   1816  endif
   1817 endfunc
   1818 
   1819 " Handle the debugged program starting to run.
   1820 " Will store the process ID in s:pid
   1821 func s:HandleProgramRun(msg)
   1822  let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0
   1823  if nr == 0
   1824    return
   1825  endif
   1826  let s:pid = nr
   1827  " call ch_log($'Detected process ID: {s:pid}')
   1828 endfunc
   1829 
   1830 " Handle a BufRead autocommand event: place any signs.
   1831 func s:BufRead()
   1832  let fname = expand('<afile>:p')
   1833  for [id, entries] in items(s:breakpoints)
   1834    for [subid, entry] in items(entries)
   1835      if entry['fname'] == fname
   1836        call s:PlaceSign(id, subid, entry)
   1837      endif
   1838    endfor
   1839  endfor
   1840 endfunc
   1841 
   1842 " Handle a BufUnloaded autocommand event: unplace any signs.
   1843 func s:BufUnloaded()
   1844  let fname = expand('<afile>:p')
   1845  for [id, entries] in items(s:breakpoints)
   1846    for [subid, entry] in items(entries)
   1847      if entry['fname'] == fname
   1848        let entry['placed'] = 0
   1849      endif
   1850    endfor
   1851  endfor
   1852 endfunc
   1853 
   1854 call s:InitHighlight()
   1855 call s:InitAutocmd()
   1856 
   1857 let &cpo = s:keepcpo
   1858 unlet s:keepcpo
   1859 
   1860 " vim: sw=2 sts=2 et