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