1" Debugger plugin using gdb. 2" 3" Author: Bram Moolenaar 4" Copyright: Vim license applies, see ":help license" 5" Last Change: 2021 Aug 06 6" 7" WORK IN PROGRESS - Only the basics work 8" Note: On MS-Windows you need a recent version of gdb. The one included with 9" MingW is too old (7.6.1). 10" I used version 7.12 from http://www.equation.com/servlet/equation.cmd?fa=gdb 11" 12" There are two ways to run gdb: 13" - In a terminal window; used if possible, does not work on MS-Windows 14" Not used when g:termdebug_use_prompt is set to 1. 15" - Using a "prompt" buffer; may use a terminal window for the program 16" 17" For both the current window is used to view source code and shows the 18" current statement from gdb. 19" 20" USING A TERMINAL WINDOW 21" 22" Opens two visible terminal windows: 23" 1. runs a pty for the debugged program, as with ":term NONE" 24" 2. runs gdb, passing the pty of the debugged program 25" A third terminal window is hidden, it is used for communication with gdb. 26" 27" USING A PROMPT BUFFER 28" 29" Opens a window with a prompt buffer to communicate with gdb. 30" Gdb is run as a job with callbacks for I/O. 31" On Unix another terminal window is opened to run the debugged program 32" On MS-Windows a separate console is opened to run the debugged program 33" 34" The communication with gdb uses GDB/MI. See: 35" https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html 36 37" In case this gets sourced twice. 38if exists(':Termdebug') 39 finish 40endif 41 42" Need either the +terminal feature or +channel and the prompt buffer. 43" The terminal feature does not work with gdb on win32. 44if has('terminal') && !has('win32') 45 let s:way = 'terminal' 46elseif has('channel') && exists('*prompt_setprompt') 47 let s:way = 'prompt' 48else 49 if has('terminal') 50 let s:err = 'Cannot debug, missing prompt buffer support' 51 else 52 let s:err = 'Cannot debug, +channel feature is not supported' 53 endif 54 command -nargs=* -complete=file -bang Termdebug echoerr s:err 55 command -nargs=+ -complete=file -bang TermdebugCommand echoerr s:err 56 finish 57endif 58 59let s:keepcpo = &cpo 60set cpo&vim 61 62" The command that starts debugging, e.g. ":Termdebug vim". 63" To end type "quit" in the gdb window. 64command -nargs=* -complete=file -bang Termdebug call s:StartDebug(<bang>0, <f-args>) 65command -nargs=+ -complete=file -bang TermdebugCommand call s:StartDebugCommand(<bang>0, <f-args>) 66 67" Name of the gdb command, defaults to "gdb". 68if !exists('g:termdebugger') 69 let g:termdebugger = 'gdb' 70endif 71 72let s:pc_id = 12 73let s:asm_id = 13 74let s:break_id = 14 " breakpoint number is added to this 75let s:stopped = 1 76 77let s:parsing_disasm_msg = 0 78let s:asm_lines = [] 79let s:asm_addr = '' 80 81" Take a breakpoint number as used by GDB and turn it into an integer. 82" The breakpoint may contain a dot: 123.4 -> 123004 83" The main breakpoint has a zero subid. 84func s:Breakpoint2SignNumber(id, subid) 85 return s:break_id + a:id * 1000 + a:subid 86endfunction 87 88func s:Highlight(init, old, new) 89 let default = a:init ? 'default ' : '' 90 if a:new ==# 'light' && a:old !=# 'light' 91 exe "hi " . default . "debugPC term=reverse ctermbg=lightblue guibg=lightblue" 92 elseif a:new ==# 'dark' && a:old !=# 'dark' 93 exe "hi " . default . "debugPC term=reverse ctermbg=darkblue guibg=darkblue" 94 endif 95endfunc 96 97call s:Highlight(1, '', &background) 98hi default debugBreakpoint term=reverse ctermbg=red guibg=red 99 100func s:StartDebug(bang, ...) 101 " First argument is the command to debug, second core file or process ID. 102 call s:StartDebug_internal({'gdb_args': a:000, 'bang': a:bang}) 103endfunc 104 105func s:StartDebugCommand(bang, ...) 106 " First argument is the command to debug, rest are run arguments. 107 call s:StartDebug_internal({'gdb_args': [a:1], 'proc_args': a:000[1:], 'bang': a:bang}) 108endfunc 109 110func s:StartDebug_internal(dict) 111 if exists('s:gdbwin') 112 echoerr 'Terminal debugger already running, cannot run two' 113 return 114 endif 115 if !executable(g:termdebugger) 116 echoerr 'Cannot execute debugger program "' .. g:termdebugger .. '"' 117 return 118 endif 119 120 let s:ptywin = 0 121 let s:pid = 0 122 let s:asmwin = 0 123 124 if exists('#User#TermdebugStartPre') 125 doauto <nomodeline> User TermdebugStartPre 126 endif 127 128 " Uncomment this line to write logging in "debuglog". 129 " call ch_logfile('debuglog', 'w') 130 131 let s:sourcewin = win_getid(winnr()) 132 133 " Remember the old value of 'signcolumn' for each buffer that it's set in, so 134 " that we can restore the value for all buffers. 135 let b:save_signcolumn = &signcolumn 136 let s:signcolumn_buflist = [bufnr()] 137 138 let s:save_columns = 0 139 let s:allleft = 0 140 if exists('g:termdebug_wide') 141 if &columns < g:termdebug_wide 142 let s:save_columns = &columns 143 let &columns = g:termdebug_wide 144 " If we make the Vim window wider, use the whole left halve for the debug 145 " windows. 146 let s:allleft = 1 147 endif 148 let s:vertical = 1 149 else 150 let s:vertical = 0 151 endif 152 153 " Override using a terminal window by setting g:termdebug_use_prompt to 1. 154 let use_prompt = exists('g:termdebug_use_prompt') && g:termdebug_use_prompt 155 if has('terminal') && !has('win32') && !use_prompt 156 let s:way = 'terminal' 157 else 158 let s:way = 'prompt' 159 endif 160 161 if s:way == 'prompt' 162 call s:StartDebug_prompt(a:dict) 163 else 164 call s:StartDebug_term(a:dict) 165 endif 166 167 if exists('g:termdebug_disasm_window') 168 if g:termdebug_disasm_window 169 let curwinid = win_getid(winnr()) 170 call s:GotoAsmwinOrCreateIt() 171 call win_gotoid(curwinid) 172 endif 173 endif 174 175 if exists('#User#TermdebugStartPost') 176 doauto <nomodeline> User TermdebugStartPost 177 endif 178endfunc 179 180" Use when debugger didn't start or ended. 181func s:CloseBuffers() 182 exe 'bwipe! ' . s:ptybuf 183 exe 'bwipe! ' . s:commbuf 184 unlet! s:gdbwin 185endfunc 186 187func s:CheckGdbRunning() 188 let gdbproc = term_getjob(s:gdbbuf) 189 if gdbproc == v:null || job_status(gdbproc) !=# 'run' 190 echoerr string(g:termdebugger) . ' exited unexpectedly' 191 call s:CloseBuffers() 192 return '' 193 endif 194 return 'ok' 195endfunc 196 197func s:StartDebug_term(dict) 198 " Open a terminal window without a job, to run the debugged program in. 199 let s:ptybuf = term_start('NONE', { 200 \ 'term_name': 'debugged program', 201 \ 'vertical': s:vertical, 202 \ }) 203 if s:ptybuf == 0 204 echoerr 'Failed to open the program terminal window' 205 return 206 endif 207 let pty = job_info(term_getjob(s:ptybuf))['tty_out'] 208 let s:ptywin = win_getid(winnr()) 209 if s:vertical 210 " Assuming the source code window will get a signcolumn, use two more 211 " columns for that, thus one less for the terminal window. 212 exe (&columns / 2 - 1) . "wincmd |" 213 if s:allleft 214 " use the whole left column 215 wincmd H 216 endif 217 endif 218 219 " Create a hidden terminal window to communicate with gdb 220 let s:commbuf = term_start('NONE', { 221 \ 'term_name': 'gdb communication', 222 \ 'out_cb': function('s:CommOutput'), 223 \ 'hidden': 1, 224 \ }) 225 if s:commbuf == 0 226 echoerr 'Failed to open the communication terminal window' 227 exe 'bwipe! ' . s:ptybuf 228 return 229 endif 230 let commpty = job_info(term_getjob(s:commbuf))['tty_out'] 231 232 " Open a terminal window to run the debugger. 233 " Add -quiet to avoid the intro message causing a hit-enter prompt. 234 let gdb_args = get(a:dict, 'gdb_args', []) 235 let proc_args = get(a:dict, 'proc_args', []) 236 237 let cmd = [g:termdebugger, '-quiet', '-tty', pty, '--eval-command', 'echo startupdone\n'] + gdb_args 238 call ch_log('executing "' . join(cmd) . '"') 239 let s:gdbbuf = term_start(cmd, { 240 \ 'term_finish': 'close', 241 \ }) 242 if s:gdbbuf == 0 243 echoerr 'Failed to open the gdb terminal window' 244 call s:CloseBuffers() 245 return 246 endif 247 let s:gdbwin = win_getid(winnr()) 248 249 " Wait for the "startupdone" message before sending any commands. 250 let try_count = 0 251 while 1 252 if s:CheckGdbRunning() != 'ok' 253 return 254 endif 255 256 for lnum in range(1, 200) 257 if term_getline(s:gdbbuf, lnum) =~ 'startupdone' 258 let try_count = 9999 259 break 260 endif 261 endfor 262 let try_count += 1 263 if try_count > 300 264 " done or give up after five seconds 265 break 266 endif 267 sleep 10m 268 endwhile 269 270 " Set arguments to be run. 271 if len(proc_args) 272 call term_sendkeys(s:gdbbuf, 'set args ' . join(proc_args) . "\r") 273 endif 274 275 " Connect gdb to the communication pty, using the GDB/MI interface 276 call term_sendkeys(s:gdbbuf, 'new-ui mi ' . commpty . "\r") 277 278 " Wait for the response to show up, users may not notice the error and wonder 279 " why the debugger doesn't work. 280 let try_count = 0 281 while 1 282 if s:CheckGdbRunning() != 'ok' 283 return 284 endif 285 286 let response = '' 287 for lnum in range(1, 200) 288 let line1 = term_getline(s:gdbbuf, lnum) 289 let line2 = term_getline(s:gdbbuf, lnum + 1) 290 if line1 =~ 'new-ui mi ' 291 " response can be in the same line or the next line 292 let response = line1 . line2 293 if response =~ 'Undefined command' 294 echoerr 'Sorry, your gdb is too old, gdb 7.12 is required' 295 call s:CloseBuffers() 296 return 297 endif 298 if response =~ 'New UI allocated' 299 " Success! 300 break 301 endif 302 elseif line1 =~ 'Reading symbols from' && line2 !~ 'new-ui mi ' 303 " Reading symbols might take a while, try more times 304 let try_count -= 1 305 endif 306 endfor 307 if response =~ 'New UI allocated' 308 break 309 endif 310 let try_count += 1 311 if try_count > 100 312 echoerr 'Cannot check if your gdb works, continuing anyway' 313 break 314 endif 315 sleep 10m 316 endwhile 317 318 " Interpret commands while the target is running. This should usually only be 319 " exec-interrupt, since many commands don't work properly while the target is 320 " running. 321 call s:SendCommand('-gdb-set mi-async on') 322 " Older gdb uses a different command. 323 call s:SendCommand('-gdb-set target-async on') 324 325 " Disable pagination, it causes everything to stop at the gdb 326 " "Type <return> to continue" prompt. 327 call s:SendCommand('set pagination off') 328 329 call job_setoptions(term_getjob(s:gdbbuf), {'exit_cb': function('s:EndTermDebug')}) 330 call s:StartDebugCommon(a:dict) 331endfunc 332 333func s:StartDebug_prompt(dict) 334 " Open a window with a prompt buffer to run gdb in. 335 if s:vertical 336 vertical new 337 else 338 new 339 endif 340 let s:gdbwin = win_getid(winnr()) 341 let s:promptbuf = bufnr('') 342 call prompt_setprompt(s:promptbuf, 'gdb> ') 343 set buftype=prompt 344 file gdb 345 call prompt_setcallback(s:promptbuf, function('s:PromptCallback')) 346 call prompt_setinterrupt(s:promptbuf, function('s:PromptInterrupt')) 347 348 if s:vertical 349 " Assuming the source code window will get a signcolumn, use two more 350 " columns for that, thus one less for the terminal window. 351 exe (&columns / 2 - 1) . "wincmd |" 352 endif 353 354 " Add -quiet to avoid the intro message causing a hit-enter prompt. 355 let gdb_args = get(a:dict, 'gdb_args', []) 356 let proc_args = get(a:dict, 'proc_args', []) 357 358 let cmd = [g:termdebugger, '-quiet', '--interpreter=mi2'] + gdb_args 359 call ch_log('executing "' . join(cmd) . '"') 360 361 let s:gdbjob = job_start(cmd, { 362 \ 'exit_cb': function('s:EndPromptDebug'), 363 \ 'out_cb': function('s:GdbOutCallback'), 364 \ }) 365 if job_status(s:gdbjob) != "run" 366 echoerr 'Failed to start gdb' 367 exe 'bwipe! ' . s:promptbuf 368 return 369 endif 370 " Mark the buffer modified so that it's not easy to close. 371 set modified 372 let s:gdb_channel = job_getchannel(s:gdbjob) 373 374 " Interpret commands while the target is running. This should usually only 375 " be exec-interrupt, since many commands don't work properly while the 376 " target is running. 377 call s:SendCommand('-gdb-set mi-async on') 378 " Older gdb uses a different command. 379 call s:SendCommand('-gdb-set target-async on') 380 381 let s:ptybuf = 0 382 if has('win32') 383 " MS-Windows: run in a new console window for maximum compatibility 384 call s:SendCommand('set new-console on') 385 elseif has('terminal') 386 " Unix: Run the debugged program in a terminal window. Open it below the 387 " gdb window. 388 belowright let s:ptybuf = term_start('NONE', { 389 \ 'term_name': 'debugged program', 390 \ }) 391 if s:ptybuf == 0 392 echoerr 'Failed to open the program terminal window' 393 call job_stop(s:gdbjob) 394 return 395 endif 396 let s:ptywin = win_getid(winnr()) 397 let pty = job_info(term_getjob(s:ptybuf))['tty_out'] 398 call s:SendCommand('tty ' . pty) 399 400 " Since GDB runs in a prompt window, the environment has not been set to 401 " match a terminal window, need to do that now. 402 call s:SendCommand('set env TERM = xterm-color') 403 call s:SendCommand('set env ROWS = ' . winheight(s:ptywin)) 404 call s:SendCommand('set env LINES = ' . winheight(s:ptywin)) 405 call s:SendCommand('set env COLUMNS = ' . winwidth(s:ptywin)) 406 call s:SendCommand('set env COLORS = ' . &t_Co) 407 call s:SendCommand('set env VIM_TERMINAL = ' . v:version) 408 else 409 " TODO: open a new terminal get get the tty name, pass on to gdb 410 call s:SendCommand('show inferior-tty') 411 endif 412 call s:SendCommand('set print pretty on') 413 call s:SendCommand('set breakpoint pending on') 414 " Disable pagination, it causes everything to stop at the gdb 415 call s:SendCommand('set pagination off') 416 417 " Set arguments to be run 418 if len(proc_args) 419 call s:SendCommand('set args ' . join(proc_args)) 420 endif 421 422 call s:StartDebugCommon(a:dict) 423 startinsert 424endfunc 425 426func s:StartDebugCommon(dict) 427 " Sign used to highlight the line where the program has stopped. 428 " There can be only one. 429 sign define debugPC linehl=debugPC 430 431 " Install debugger commands in the text window. 432 call win_gotoid(s:sourcewin) 433 call s:InstallCommands() 434 call win_gotoid(s:gdbwin) 435 436 " Enable showing a balloon with eval info 437 if has("balloon_eval") || has("balloon_eval_term") 438 set balloonexpr=TermDebugBalloonExpr() 439 if has("balloon_eval") 440 set ballooneval 441 endif 442 if has("balloon_eval_term") 443 set balloonevalterm 444 endif 445 endif 446 447 " Contains breakpoints that have been placed, key is a string with the GDB 448 " breakpoint number. 449 " Each entry is a dict, containing the sub-breakpoints. Key is the subid. 450 " For a breakpoint that is just a number the subid is zero. 451 " For a breakpoint "123.4" the id is "123" and subid is "4". 452 " Example, when breakpoint "44", "123", "123.1" and "123.2" exist: 453 " {'44': {'0': entry}, '123': {'0': entry, '1': entry, '2': entry}} 454 let s:breakpoints = {} 455 456 " Contains breakpoints by file/lnum. The key is "fname:lnum". 457 " Each entry is a list of breakpoint IDs at that position. 458 let s:breakpoint_locations = {} 459 460 augroup TermDebug 461 au BufRead * call s:BufRead() 462 au BufUnload * call s:BufUnloaded() 463 au OptionSet background call s:Highlight(0, v:option_old, v:option_new) 464 augroup END 465 466 " Run the command if the bang attribute was given and got to the debug 467 " window. 468 if get(a:dict, 'bang', 0) 469 call s:SendCommand('-exec-run') 470 call win_gotoid(s:ptywin) 471 endif 472endfunc 473 474" Send a command to gdb. "cmd" is the string without line terminator. 475func s:SendCommand(cmd) 476 call ch_log('sending to gdb: ' . a:cmd) 477 if s:way == 'prompt' 478 call ch_sendraw(s:gdb_channel, a:cmd . "\n") 479 else 480 call term_sendkeys(s:commbuf, a:cmd . "\r") 481 endif 482endfunc 483 484" This is global so that a user can create their mappings with this. 485func TermDebugSendCommand(cmd) 486 if s:way == 'prompt' 487 call ch_sendraw(s:gdb_channel, a:cmd . "\n") 488 else 489 let do_continue = 0 490 if !s:stopped 491 let do_continue = 1 492 call s:SendCommand('-exec-interrupt') 493 sleep 10m 494 endif 495 call term_sendkeys(s:gdbbuf, a:cmd . "\r") 496 if do_continue 497 Continue 498 endif 499 endif 500endfunc 501 502" Function called when entering a line in the prompt buffer. 503func s:PromptCallback(text) 504 call s:SendCommand(a:text) 505endfunc 506 507" Function called when pressing CTRL-C in the prompt buffer and when placing a 508" breakpoint. 509func s:PromptInterrupt() 510 call ch_log('Interrupting gdb') 511 if has('win32') 512 " Using job_stop() does not work on MS-Windows, need to send SIGTRAP to 513 " the debugger program so that gdb responds again. 514 if s:pid == 0 515 echoerr 'Cannot interrupt gdb, did not find a process ID' 516 else 517 call debugbreak(s:pid) 518 endif 519 else 520 call job_stop(s:gdbjob, 'int') 521 endif 522endfunc 523 524" Function called when gdb outputs text. 525func s:GdbOutCallback(channel, text) 526 call ch_log('received from gdb: ' . a:text) 527 528 " Drop the gdb prompt, we have our own. 529 " Drop status and echo'd commands. 530 if a:text == '(gdb) ' || a:text == '^done' || a:text[0] == '&' 531 return 532 endif 533 if a:text =~ '^^error,msg=' 534 let text = s:DecodeMessage(a:text[11:]) 535 if exists('s:evalexpr') && text =~ 'A syntax error in expression, near\|No symbol .* in current context' 536 " Silently drop evaluation errors. 537 unlet s:evalexpr 538 return 539 endif 540 elseif a:text[0] == '~' 541 let text = s:DecodeMessage(a:text[1:]) 542 else 543 call s:CommOutput(a:channel, a:text) 544 return 545 endif 546 547 let curwinid = win_getid(winnr()) 548 call win_gotoid(s:gdbwin) 549 550 " Add the output above the current prompt. 551 call append(line('$') - 1, text) 552 set modified 553 554 call win_gotoid(curwinid) 555endfunc 556 557" Decode a message from gdb. quotedText starts with a ", return the text up 558" to the next ", unescaping characters. 559func s:DecodeMessage(quotedText) 560 if a:quotedText[0] != '"' 561 echoerr 'DecodeMessage(): missing quote in ' . a:quotedText 562 return 563 endif 564 let result = '' 565 let i = 1 566 while a:quotedText[i] != '"' && i < len(a:quotedText) 567 if a:quotedText[i] == '\' 568 let i += 1 569 if a:quotedText[i] == 'n' 570 " drop \n 571 let i += 1 572 continue 573 elseif a:quotedText[i] == 't' 574 " append \t 575 let i += 1 576 let result .= "\t" 577 continue 578 endif 579 endif 580 let result .= a:quotedText[i] 581 let i += 1 582 endwhile 583 return result 584endfunc 585 586" Extract the "name" value from a gdb message with fullname="name". 587func s:GetFullname(msg) 588 if a:msg !~ 'fullname' 589 return '' 590 endif 591 let name = s:DecodeMessage(substitute(a:msg, '.*fullname=', '', '')) 592 if has('win32') && name =~ ':\\\\' 593 " sometimes the name arrives double-escaped 594 let name = substitute(name, '\\\\', '\\', 'g') 595 endif 596 return name 597endfunc 598 599" Extract the "addr" value from a gdb message with addr="0x0001234". 600func s:GetAsmAddr(msg) 601 if a:msg !~ 'addr=' 602 return '' 603 endif 604 let addr = s:DecodeMessage(substitute(a:msg, '.*addr=', '', '')) 605 return addr 606endfunc 607 608func s:EndTermDebug(job, status) 609 if exists('#User#TermdebugStopPre') 610 doauto <nomodeline> User TermdebugStopPre 611 endif 612 613 exe 'bwipe! ' . s:commbuf 614 unlet s:gdbwin 615 616 call s:EndDebugCommon() 617endfunc 618 619func s:EndDebugCommon() 620 let curwinid = win_getid(winnr()) 621 622 if exists('s:ptybuf') && s:ptybuf 623 exe 'bwipe! ' . s:ptybuf 624 endif 625 626 " Restore 'signcolumn' in all buffers for which it was set. 627 call win_gotoid(s:sourcewin) 628 let was_buf = bufnr() 629 for bufnr in s:signcolumn_buflist 630 if bufexists(bufnr) 631 exe bufnr .. "buf" 632 if exists('b:save_signcolumn') 633 let &signcolumn = b:save_signcolumn 634 unlet b:save_signcolumn 635 endif 636 endif 637 endfor 638 exe was_buf .. "buf" 639 640 call s:DeleteCommands() 641 642 call win_gotoid(curwinid) 643 644 if s:save_columns > 0 645 let &columns = s:save_columns 646 endif 647 648 if has("balloon_eval") || has("balloon_eval_term") 649 set balloonexpr= 650 if has("balloon_eval") 651 set noballooneval 652 endif 653 if has("balloon_eval_term") 654 set noballoonevalterm 655 endif 656 endif 657 658 if exists('#User#TermdebugStopPost') 659 doauto <nomodeline> User TermdebugStopPost 660 endif 661 662 au! TermDebug 663endfunc 664 665func s:EndPromptDebug(job, status) 666 if exists('#User#TermdebugStopPre') 667 doauto <nomodeline> User TermdebugStopPre 668 endif 669 670 let curwinid = win_getid(winnr()) 671 call win_gotoid(s:gdbwin) 672 set nomodified 673 close 674 if curwinid != s:gdbwin 675 call win_gotoid(curwinid) 676 endif 677 678 call s:EndDebugCommon() 679 unlet s:gdbwin 680 call ch_log("Returning from EndPromptDebug()") 681endfunc 682 683" Disassembly window - added by Michael Sartain 684" 685" - CommOutput: disassemble $pc 686" - CommOutput: &"disassemble $pc\n" 687" - CommOutput: ~"Dump of assembler code for function main(int, char**):\n" 688" - CommOutput: ~" 0x0000555556466f69 <+0>:\tpush rbp\n" 689" ... 690" - CommOutput: ~" 0x0000555556467cd0:\tpop rbp\n" 691" - CommOutput: ~" 0x0000555556467cd1:\tret \n" 692" - CommOutput: ~"End of assembler dump.\n" 693" - CommOutput: ^done 694 695" - CommOutput: disassemble $pc 696" - CommOutput: &"disassemble $pc\n" 697" - CommOutput: &"No function contains specified address.\n" 698" - CommOutput: ^error,msg="No function contains specified address." 699func s:HandleDisasmMsg(msg) 700 if a:msg =~ '^\^done' 701 let curwinid = win_getid(winnr()) 702 if win_gotoid(s:asmwin) 703 silent normal! gg0"_dG 704 call setline(1, s:asm_lines) 705 set nomodified 706 set filetype=asm 707 708 let lnum = search('^' . s:asm_addr) 709 if lnum != 0 710 exe 'sign unplace ' . s:asm_id 711 exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC' 712 endif 713 714 call win_gotoid(curwinid) 715 endif 716 717 let s:parsing_disasm_msg = 0 718 let s:asm_lines = [] 719 elseif a:msg =~ '^\^error,msg=' 720 if s:parsing_disasm_msg == 1 721 " Disassemble call ran into an error. This can happen when gdb can't 722 " find the function frame address, so let's try to disassemble starting 723 " at current PC 724 call s:SendCommand('disassemble $pc,+100') 725 endif 726 let s:parsing_disasm_msg = 0 727 elseif a:msg =~ '\&\"disassemble \$pc' 728 if a:msg =~ '+100' 729 " This is our second disasm attempt 730 let s:parsing_disasm_msg = 2 731 endif 732 else 733 let value = substitute(a:msg, '^\~\"[ ]*', '', '') 734 let value = substitute(value, '^=>[ ]*', '', '') 735 let value = substitute(value, '\\n\" 736$', '', '') 737 let value = substitute(value, '\\n\"$', '', '') 738 let value = substitute(value, ' 739', '', '') 740 let value = substitute(value, '\\t', ' ', 'g') 741 742 if value != '' || !empty(s:asm_lines) 743 call add(s:asm_lines, value) 744 endif 745 endif 746endfunc 747 748" Handle a message received from gdb on the GDB/MI interface. 749func s:CommOutput(chan, msg) 750 let msgs = split(a:msg, "\r") 751 752 for msg in msgs 753 " remove prefixed NL 754 if msg[0] == "\n" 755 let msg = msg[1:] 756 endif 757 758 if s:parsing_disasm_msg 759 call s:HandleDisasmMsg(msg) 760 elseif msg != '' 761 if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)' 762 call s:HandleCursor(msg) 763 elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,' 764 call s:HandleNewBreakpoint(msg) 765 elseif msg =~ '^=breakpoint-deleted,' 766 call s:HandleBreakpointDelete(msg) 767 elseif msg =~ '^=thread-group-started' 768 call s:HandleProgramRun(msg) 769 elseif msg =~ '^\^done,value=' 770 call s:HandleEvaluate(msg) 771 elseif msg =~ '^\^error,msg=' 772 call s:HandleError(msg) 773 elseif msg =~ '^disassemble' 774 let s:parsing_disasm_msg = 1 775 let s:asm_lines = [] 776 endif 777 endif 778 endfor 779endfunc 780 781func s:GotoProgram() 782 if has('win32') 783 if executable('powershell') 784 call system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', s:pid)) 785 endif 786 else 787 call win_gotoid(s:ptywin) 788 endif 789endfunc 790 791" Install commands in the current window to control the debugger. 792func s:InstallCommands() 793 let save_cpo = &cpo 794 set cpo&vim 795 796 command -nargs=? Break call s:SetBreakpoint(<q-args>) 797 command Clear call s:ClearBreakpoint() 798 command Step call s:SendCommand('-exec-step') 799 command Over call s:SendCommand('-exec-next') 800 command Finish call s:SendCommand('-exec-finish') 801 command -nargs=* Run call s:Run(<q-args>) 802 command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>) 803 command Stop call s:SendCommand('-exec-interrupt') 804 805 " using -exec-continue results in CTRL-C in gdb window not working 806 if s:way == 'prompt' 807 command Continue call s:SendCommand('continue') 808 else 809 command Continue call term_sendkeys(s:gdbbuf, "continue\r") 810 endif 811 812 command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>) 813 command Gdb call win_gotoid(s:gdbwin) 814 command Program call s:GotoProgram() 815 command Source call s:GotoSourcewinOrCreateIt() 816 command Asm call s:GotoAsmwinOrCreateIt() 817 command Winbar call s:InstallWinbar() 818 819 if !exists('g:termdebug_map_K') || g:termdebug_map_K 820 let s:k_map_saved = maparg('K', 'n', 0, 1) 821 nnoremap K :Evaluate<CR> 822 endif 823 824 if has('menu') && &mouse != '' 825 call s:InstallWinbar() 826 827 if !exists('g:termdebug_popup') || g:termdebug_popup != 0 828 let s:saved_mousemodel = &mousemodel 829 let &mousemodel = 'popup_setpos' 830 an 1.200 PopUp.-SEP3- <Nop> 831 an 1.210 PopUp.Set\ breakpoint :Break<CR> 832 an 1.220 PopUp.Clear\ breakpoint :Clear<CR> 833 an 1.230 PopUp.Evaluate :Evaluate<CR> 834 endif 835 endif 836 837 let &cpo = save_cpo 838endfunc 839 840let s:winbar_winids = [] 841 842" Install the window toolbar in the current window. 843func s:InstallWinbar() 844 if has('menu') && &mouse != '' 845 nnoremenu WinBar.Step :Step<CR> 846 nnoremenu WinBar.Next :Over<CR> 847 nnoremenu WinBar.Finish :Finish<CR> 848 nnoremenu WinBar.Cont :Continue<CR> 849 nnoremenu WinBar.Stop :Stop<CR> 850 nnoremenu WinBar.Eval :Evaluate<CR> 851 call add(s:winbar_winids, win_getid(winnr())) 852 endif 853endfunc 854 855" Delete installed debugger commands in the current window. 856func s:DeleteCommands() 857 delcommand Break 858 delcommand Clear 859 delcommand Step 860 delcommand Over 861 delcommand Finish 862 delcommand Run 863 delcommand Arguments 864 delcommand Stop 865 delcommand Continue 866 delcommand Evaluate 867 delcommand Gdb 868 delcommand Program 869 delcommand Source 870 delcommand Asm 871 delcommand Winbar 872 873 if exists('s:k_map_saved') 874 if empty(s:k_map_saved) 875 nunmap K 876 else 877 call mapset('n', 0, s:k_map_saved) 878 endif 879 unlet s:k_map_saved 880 endif 881 882 if has('menu') 883 " Remove the WinBar entries from all windows where it was added. 884 let curwinid = win_getid(winnr()) 885 for winid in s:winbar_winids 886 if win_gotoid(winid) 887 aunmenu WinBar.Step 888 aunmenu WinBar.Next 889 aunmenu WinBar.Finish 890 aunmenu WinBar.Cont 891 aunmenu WinBar.Stop 892 aunmenu WinBar.Eval 893 endif 894 endfor 895 call win_gotoid(curwinid) 896 let s:winbar_winids = [] 897 898 if exists('s:saved_mousemodel') 899 let &mousemodel = s:saved_mousemodel 900 unlet s:saved_mousemodel 901 aunmenu PopUp.-SEP3- 902 aunmenu PopUp.Set\ breakpoint 903 aunmenu PopUp.Clear\ breakpoint 904 aunmenu PopUp.Evaluate 905 endif 906 endif 907 908 exe 'sign unplace ' . s:pc_id 909 for [id, entries] in items(s:breakpoints) 910 for subid in keys(entries) 911 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 912 endfor 913 endfor 914 unlet s:breakpoints 915 unlet s:breakpoint_locations 916 917 sign undefine debugPC 918 for val in s:BreakpointSigns 919 exe "sign undefine debugBreakpoint" . val 920 endfor 921 let s:BreakpointSigns = [] 922endfunc 923 924" :Break - Set a breakpoint at the cursor position. 925func s:SetBreakpoint(at) 926 " Setting a breakpoint may not work while the program is running. 927 " Interrupt to make it work. 928 let do_continue = 0 929 if !s:stopped 930 let do_continue = 1 931 if s:way == 'prompt' 932 call s:PromptInterrupt() 933 else 934 call s:SendCommand('-exec-interrupt') 935 endif 936 sleep 10m 937 endif 938 939 " Use the fname:lnum format, older gdb can't handle --source. 940 let at = empty(a:at) ? 941 \ fnameescape(expand('%:p')) . ':' . line('.') : a:at 942 call s:SendCommand('-break-insert ' . at) 943 if do_continue 944 call s:SendCommand('-exec-continue') 945 endif 946endfunc 947 948" :Clear - Delete a breakpoint at the cursor position. 949func s:ClearBreakpoint() 950 let fname = fnameescape(expand('%:p')) 951 let lnum = line('.') 952 let bploc = printf('%s:%d', fname, lnum) 953 if has_key(s:breakpoint_locations, bploc) 954 let idx = 0 955 for id in s:breakpoint_locations[bploc] 956 if has_key(s:breakpoints, id) 957 " Assume this always works, the reply is simply "^done". 958 call s:SendCommand('-break-delete ' . id) 959 for subid in keys(s:breakpoints[id]) 960 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 961 endfor 962 unlet s:breakpoints[id] 963 unlet s:breakpoint_locations[bploc][idx] 964 break 965 else 966 let idx += 1 967 endif 968 endfor 969 if empty(s:breakpoint_locations[bploc]) 970 unlet s:breakpoint_locations[bploc] 971 endif 972 endif 973endfunc 974 975func s:Run(args) 976 if a:args != '' 977 call s:SendCommand('-exec-arguments ' . a:args) 978 endif 979 call s:SendCommand('-exec-run') 980endfunc 981 982func s:SendEval(expr) 983 call s:SendCommand('-data-evaluate-expression "' . a:expr . '"') 984 let s:evalexpr = a:expr 985endfunc 986 987" :Evaluate - evaluate what is under the cursor 988func s:Evaluate(range, arg) 989 if a:arg != '' 990 let expr = a:arg 991 elseif a:range == 2 992 let pos = getcurpos() 993 let reg = getreg('v', 1, 1) 994 let regt = getregtype('v') 995 normal! gv"vy 996 let expr = @v 997 call setpos('.', pos) 998 call setreg('v', reg, regt) 999 else 1000 let expr = expand('<cexpr>') 1001 endif 1002 let s:ignoreEvalError = 0 1003 call s:SendEval(expr) 1004endfunc 1005 1006let s:ignoreEvalError = 0 1007let s:evalFromBalloonExpr = 0 1008 1009" Handle the result of data-evaluate-expression 1010func s:HandleEvaluate(msg) 1011 let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '') 1012 let value = substitute(value, '\\"', '"', 'g') 1013 if s:evalFromBalloonExpr 1014 if s:evalFromBalloonExprResult == '' 1015 let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value 1016 else 1017 let s:evalFromBalloonExprResult .= ' = ' . value 1018 endif 1019 call balloon_show(s:evalFromBalloonExprResult) 1020 else 1021 echomsg '"' . s:evalexpr . '": ' . value 1022 endif 1023 1024 if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$' 1025 " Looks like a pointer, also display what it points to. 1026 let s:ignoreEvalError = 1 1027 call s:SendEval('*' . s:evalexpr) 1028 else 1029 let s:evalFromBalloonExpr = 0 1030 endif 1031endfunc 1032 1033" Show a balloon with information of the variable under the mouse pointer, 1034" if there is any. 1035func TermDebugBalloonExpr() 1036 if v:beval_winid != s:sourcewin 1037 return '' 1038 endif 1039 if !s:stopped 1040 " Only evaluate when stopped, otherwise setting a breakpoint using the 1041 " mouse triggers a balloon. 1042 return '' 1043 endif 1044 let s:evalFromBalloonExpr = 1 1045 let s:evalFromBalloonExprResult = '' 1046 let s:ignoreEvalError = 1 1047 call s:SendEval(v:beval_text) 1048 return '' 1049endfunc 1050 1051" Handle an error. 1052func s:HandleError(msg) 1053 if s:ignoreEvalError 1054 " Result of s:SendEval() failed, ignore. 1055 let s:ignoreEvalError = 0 1056 let s:evalFromBalloonExpr = 0 1057 return 1058 endif 1059 echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '') 1060endfunc 1061 1062func s:GotoSourcewinOrCreateIt() 1063 if !win_gotoid(s:sourcewin) 1064 new 1065 let s:sourcewin = win_getid(winnr()) 1066 call s:InstallWinbar() 1067 endif 1068endfunc 1069 1070func s:GotoAsmwinOrCreateIt() 1071 if !win_gotoid(s:asmwin) 1072 if win_gotoid(s:sourcewin) 1073 exe 'rightbelow new' 1074 else 1075 exe 'new' 1076 endif 1077 1078 let s:asmwin = win_getid(winnr()) 1079 1080 setlocal nowrap 1081 setlocal number 1082 setlocal noswapfile 1083 setlocal buftype=nofile 1084 1085 let asmbuf = bufnr('Termdebug-asm-listing') 1086 if asmbuf > 0 1087 exe 'buffer' . asmbuf 1088 else 1089 exe 'file Termdebug-asm-listing' 1090 endif 1091 1092 if exists('g:termdebug_disasm_window') 1093 if g:termdebug_disasm_window > 1 1094 exe 'resize ' . g:termdebug_disasm_window 1095 endif 1096 endif 1097 endif 1098 1099 if s:asm_addr != '' 1100 let lnum = search('^' . s:asm_addr) 1101 if lnum == 0 1102 if s:stopped 1103 call s:SendCommand('disassemble $pc') 1104 endif 1105 else 1106 exe 'sign unplace ' . s:asm_id 1107 exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC' 1108 endif 1109 endif 1110endfunc 1111 1112" Handle stopping and running message from gdb. 1113" Will update the sign that shows the current position. 1114func s:HandleCursor(msg) 1115 let wid = win_getid(winnr()) 1116 1117 if a:msg =~ '^\*stopped' 1118 call ch_log('program stopped') 1119 let s:stopped = 1 1120 elseif a:msg =~ '^\*running' 1121 call ch_log('program running') 1122 let s:stopped = 0 1123 endif 1124 1125 if a:msg =~ 'fullname=' 1126 let fname = s:GetFullname(a:msg) 1127 else 1128 let fname = '' 1129 endif 1130 1131 if a:msg =~ 'addr=' 1132 let asm_addr = s:GetAsmAddr(a:msg) 1133 if asm_addr != '' 1134 let s:asm_addr = asm_addr 1135 1136 let curwinid = win_getid(winnr()) 1137 if win_gotoid(s:asmwin) 1138 let lnum = search('^' . s:asm_addr) 1139 if lnum == 0 1140 call s:SendCommand('disassemble $pc') 1141 else 1142 exe 'sign unplace ' . s:asm_id 1143 exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC' 1144 endif 1145 1146 call win_gotoid(curwinid) 1147 endif 1148 endif 1149 endif 1150 1151 if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) 1152 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 1153 if lnum =~ '^[0-9]*$' 1154 call s:GotoSourcewinOrCreateIt() 1155 if expand('%:p') != fnamemodify(fname, ':p') 1156 if &modified 1157 " TODO: find existing window 1158 exe 'split ' . fnameescape(fname) 1159 let s:sourcewin = win_getid(winnr()) 1160 call s:InstallWinbar() 1161 else 1162 exe 'edit ' . fnameescape(fname) 1163 endif 1164 endif 1165 exe lnum 1166 exe 'sign unplace ' . s:pc_id 1167 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC priority=110 file=' . fname 1168 if !exists('b:save_signcolumn') 1169 let b:save_signcolumn = &signcolumn 1170 call add(s:signcolumn_buflist, bufnr()) 1171 endif 1172 setlocal signcolumn=yes 1173 endif 1174 elseif !s:stopped || fname != '' 1175 exe 'sign unplace ' . s:pc_id 1176 endif 1177 1178 call win_gotoid(wid) 1179endfunc 1180 1181let s:BreakpointSigns = [] 1182 1183func s:CreateBreakpoint(id, subid) 1184 let nr = printf('%d.%d', a:id, a:subid) 1185 if index(s:BreakpointSigns, nr) == -1 1186 call add(s:BreakpointSigns, nr) 1187 exe "sign define debugBreakpoint" . nr . " text=" . substitute(nr, '\..*', '', '') . " texthl=debugBreakpoint" 1188 endif 1189endfunc 1190 1191func! s:SplitMsg(s) 1192 return split(a:s, '{.\{-}}\zs') 1193endfunction 1194 1195" Handle setting a breakpoint 1196" Will update the sign that shows the breakpoint 1197func s:HandleNewBreakpoint(msg) 1198 if a:msg !~ 'fullname=' 1199 " a watch does not have a file name 1200 return 1201 endif 1202 for msg in s:SplitMsg(a:msg) 1203 let fname = s:GetFullname(msg) 1204 if empty(fname) 1205 continue 1206 endif 1207 let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '') 1208 if empty(nr) 1209 return 1210 endif 1211 1212 " If "nr" is 123 it becomes "123.0" and subid is "0". 1213 " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded. 1214 let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0') 1215 call s:CreateBreakpoint(id, subid) 1216 1217 if has_key(s:breakpoints, id) 1218 let entries = s:breakpoints[id] 1219 else 1220 let entries = {} 1221 let s:breakpoints[id] = entries 1222 endif 1223 if has_key(entries, subid) 1224 let entry = entries[subid] 1225 else 1226 let entry = {} 1227 let entries[subid] = entry 1228 endif 1229 1230 let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '') 1231 let entry['fname'] = fname 1232 let entry['lnum'] = lnum 1233 1234 let bploc = printf('%s:%d', fname, lnum) 1235 if !has_key(s:breakpoint_locations, bploc) 1236 let s:breakpoint_locations[bploc] = [] 1237 endif 1238 let s:breakpoint_locations[bploc] += [id] 1239 1240 if bufloaded(fname) 1241 call s:PlaceSign(id, subid, entry) 1242 endif 1243 endfor 1244endfunc 1245 1246func s:PlaceSign(id, subid, entry) 1247 let nr = printf('%d.%d', a:id, a:subid) 1248 exe 'sign place ' . s:Breakpoint2SignNumber(a:id, a:subid) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . nr . ' priority=110 file=' . a:entry['fname'] 1249 let a:entry['placed'] = 1 1250endfunc 1251 1252" Handle deleting a breakpoint 1253" Will remove the sign that shows the breakpoint 1254func s:HandleBreakpointDelete(msg) 1255 let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 1256 if empty(id) 1257 return 1258 endif 1259 if has_key(s:breakpoints, id) 1260 for [subid, entry] in items(s:breakpoints[id]) 1261 if has_key(entry, 'placed') 1262 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 1263 unlet entry['placed'] 1264 endif 1265 endfor 1266 unlet s:breakpoints[id] 1267 endif 1268endfunc 1269 1270" Handle the debugged program starting to run. 1271" Will store the process ID in s:pid 1272func s:HandleProgramRun(msg) 1273 let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0 1274 if nr == 0 1275 return 1276 endif 1277 let s:pid = nr 1278 call ch_log('Detected process ID: ' . s:pid) 1279endfunc 1280 1281" Handle a BufRead autocommand event: place any signs. 1282func s:BufRead() 1283 let fname = expand('<afile>:p') 1284 for [id, entries] in items(s:breakpoints) 1285 for [subid, entry] in items(entries) 1286 if entry['fname'] == fname 1287 call s:PlaceSign(id, subid, entry) 1288 endif 1289 endfor 1290 endfor 1291endfunc 1292 1293" Handle a BufUnloaded autocommand event: unplace any signs. 1294func s:BufUnloaded() 1295 let fname = expand('<afile>:p') 1296 for [id, entries] in items(s:breakpoints) 1297 for [subid, entry] in items(entries) 1298 if entry['fname'] == fname 1299 let entry['placed'] = 0 1300 endif 1301 endfor 1302 endfor 1303endfunc 1304 1305let &cpo = s:keepcpo 1306unlet s:keepcpo 1307