1" Debugger plugin using gdb. 2" 3" Author: Bram Moolenaar 4" Copyright: Vim license applies, see ":help license" 5" Last Change: 2021 Aug 23 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 331 " Set the filetype, this can be used to add mappings. 332 set filetype=termdebug 333 334 call s:StartDebugCommon(a:dict) 335endfunc 336 337func s:StartDebug_prompt(dict) 338 " Open a window with a prompt buffer to run gdb in. 339 if s:vertical 340 vertical new 341 else 342 new 343 endif 344 let s:gdbwin = win_getid(winnr()) 345 let s:promptbuf = bufnr('') 346 call prompt_setprompt(s:promptbuf, 'gdb> ') 347 set buftype=prompt 348 file gdb 349 call prompt_setcallback(s:promptbuf, function('s:PromptCallback')) 350 call prompt_setinterrupt(s:promptbuf, function('s:PromptInterrupt')) 351 352 if s:vertical 353 " Assuming the source code window will get a signcolumn, use two more 354 " columns for that, thus one less for the terminal window. 355 exe (&columns / 2 - 1) . "wincmd |" 356 endif 357 358 " Add -quiet to avoid the intro message causing a hit-enter prompt. 359 let gdb_args = get(a:dict, 'gdb_args', []) 360 let proc_args = get(a:dict, 'proc_args', []) 361 362 let cmd = [g:termdebugger, '-quiet', '--interpreter=mi2'] + gdb_args 363 call ch_log('executing "' . join(cmd) . '"') 364 365 let s:gdbjob = job_start(cmd, { 366 \ 'exit_cb': function('s:EndPromptDebug'), 367 \ 'out_cb': function('s:GdbOutCallback'), 368 \ }) 369 if job_status(s:gdbjob) != "run" 370 echoerr 'Failed to start gdb' 371 exe 'bwipe! ' . s:promptbuf 372 return 373 endif 374 " Mark the buffer modified so that it's not easy to close. 375 set modified 376 let s:gdb_channel = job_getchannel(s:gdbjob) 377 378 " Interpret commands while the target is running. This should usually only 379 " be exec-interrupt, since many commands don't work properly while the 380 " target is running. 381 call s:SendCommand('-gdb-set mi-async on') 382 " Older gdb uses a different command. 383 call s:SendCommand('-gdb-set target-async on') 384 385 let s:ptybuf = 0 386 if has('win32') 387 " MS-Windows: run in a new console window for maximum compatibility 388 call s:SendCommand('set new-console on') 389 elseif has('terminal') 390 " Unix: Run the debugged program in a terminal window. Open it below the 391 " gdb window. 392 belowright let s:ptybuf = term_start('NONE', { 393 \ 'term_name': 'debugged program', 394 \ }) 395 if s:ptybuf == 0 396 echoerr 'Failed to open the program terminal window' 397 call job_stop(s:gdbjob) 398 return 399 endif 400 let s:ptywin = win_getid(winnr()) 401 let pty = job_info(term_getjob(s:ptybuf))['tty_out'] 402 call s:SendCommand('tty ' . pty) 403 404 " Since GDB runs in a prompt window, the environment has not been set to 405 " match a terminal window, need to do that now. 406 call s:SendCommand('set env TERM = xterm-color') 407 call s:SendCommand('set env ROWS = ' . winheight(s:ptywin)) 408 call s:SendCommand('set env LINES = ' . winheight(s:ptywin)) 409 call s:SendCommand('set env COLUMNS = ' . winwidth(s:ptywin)) 410 call s:SendCommand('set env COLORS = ' . &t_Co) 411 call s:SendCommand('set env VIM_TERMINAL = ' . v:version) 412 else 413 " TODO: open a new terminal get get the tty name, pass on to gdb 414 call s:SendCommand('show inferior-tty') 415 endif 416 call s:SendCommand('set print pretty on') 417 call s:SendCommand('set breakpoint pending on') 418 " Disable pagination, it causes everything to stop at the gdb 419 call s:SendCommand('set pagination off') 420 421 " Set arguments to be run 422 if len(proc_args) 423 call s:SendCommand('set args ' . join(proc_args)) 424 endif 425 426 call s:StartDebugCommon(a:dict) 427 startinsert 428endfunc 429 430func s:StartDebugCommon(dict) 431 " Sign used to highlight the line where the program has stopped. 432 " There can be only one. 433 sign define debugPC linehl=debugPC 434 435 " Install debugger commands in the text window. 436 call win_gotoid(s:sourcewin) 437 call s:InstallCommands() 438 call win_gotoid(s:gdbwin) 439 440 " Enable showing a balloon with eval info 441 if has("balloon_eval") || has("balloon_eval_term") 442 set balloonexpr=TermDebugBalloonExpr() 443 if has("balloon_eval") 444 set ballooneval 445 endif 446 if has("balloon_eval_term") 447 set balloonevalterm 448 endif 449 endif 450 451 " Contains breakpoints that have been placed, key is a string with the GDB 452 " breakpoint number. 453 " Each entry is a dict, containing the sub-breakpoints. Key is the subid. 454 " For a breakpoint that is just a number the subid is zero. 455 " For a breakpoint "123.4" the id is "123" and subid is "4". 456 " Example, when breakpoint "44", "123", "123.1" and "123.2" exist: 457 " {'44': {'0': entry}, '123': {'0': entry, '1': entry, '2': entry}} 458 let s:breakpoints = {} 459 460 " Contains breakpoints by file/lnum. The key is "fname:lnum". 461 " Each entry is a list of breakpoint IDs at that position. 462 let s:breakpoint_locations = {} 463 464 augroup TermDebug 465 au BufRead * call s:BufRead() 466 au BufUnload * call s:BufUnloaded() 467 au OptionSet background call s:Highlight(0, v:option_old, v:option_new) 468 augroup END 469 470 " Run the command if the bang attribute was given and got to the debug 471 " window. 472 if get(a:dict, 'bang', 0) 473 call s:SendCommand('-exec-run') 474 call win_gotoid(s:ptywin) 475 endif 476endfunc 477 478" Send a command to gdb. "cmd" is the string without line terminator. 479func s:SendCommand(cmd) 480 call ch_log('sending to gdb: ' . a:cmd) 481 if s:way == 'prompt' 482 call ch_sendraw(s:gdb_channel, a:cmd . "\n") 483 else 484 call term_sendkeys(s:commbuf, a:cmd . "\r") 485 endif 486endfunc 487 488" This is global so that a user can create their mappings with this. 489func TermDebugSendCommand(cmd) 490 if s:way == 'prompt' 491 call ch_sendraw(s:gdb_channel, a:cmd . "\n") 492 else 493 let do_continue = 0 494 if !s:stopped 495 let do_continue = 1 496 call s:SendCommand('-exec-interrupt') 497 sleep 10m 498 endif 499 call term_sendkeys(s:gdbbuf, a:cmd . "\r") 500 if do_continue 501 Continue 502 endif 503 endif 504endfunc 505 506" Function called when entering a line in the prompt buffer. 507func s:PromptCallback(text) 508 call s:SendCommand(a:text) 509endfunc 510 511" Function called when pressing CTRL-C in the prompt buffer and when placing a 512" breakpoint. 513func s:PromptInterrupt() 514 call ch_log('Interrupting gdb') 515 if has('win32') 516 " Using job_stop() does not work on MS-Windows, need to send SIGTRAP to 517 " the debugger program so that gdb responds again. 518 if s:pid == 0 519 echoerr 'Cannot interrupt gdb, did not find a process ID' 520 else 521 call debugbreak(s:pid) 522 endif 523 else 524 call job_stop(s:gdbjob, 'int') 525 endif 526endfunc 527 528" Function called when gdb outputs text. 529func s:GdbOutCallback(channel, text) 530 call ch_log('received from gdb: ' . a:text) 531 532 " Drop the gdb prompt, we have our own. 533 " Drop status and echo'd commands. 534 if a:text == '(gdb) ' || a:text == '^done' || a:text[0] == '&' 535 return 536 endif 537 if a:text =~ '^^error,msg=' 538 let text = s:DecodeMessage(a:text[11:]) 539 if exists('s:evalexpr') && text =~ 'A syntax error in expression, near\|No symbol .* in current context' 540 " Silently drop evaluation errors. 541 unlet s:evalexpr 542 return 543 endif 544 elseif a:text[0] == '~' 545 let text = s:DecodeMessage(a:text[1:]) 546 else 547 call s:CommOutput(a:channel, a:text) 548 return 549 endif 550 551 let curwinid = win_getid(winnr()) 552 call win_gotoid(s:gdbwin) 553 554 " Add the output above the current prompt. 555 call append(line('$') - 1, text) 556 set modified 557 558 call win_gotoid(curwinid) 559endfunc 560 561" Decode a message from gdb. quotedText starts with a ", return the text up 562" to the next ", unescaping characters. 563func s:DecodeMessage(quotedText) 564 if a:quotedText[0] != '"' 565 echoerr 'DecodeMessage(): missing quote in ' . a:quotedText 566 return 567 endif 568 let result = '' 569 let i = 1 570 while a:quotedText[i] != '"' && i < len(a:quotedText) 571 if a:quotedText[i] == '\' 572 let i += 1 573 if a:quotedText[i] == 'n' 574 " drop \n 575 let i += 1 576 continue 577 elseif a:quotedText[i] == 't' 578 " append \t 579 let i += 1 580 let result .= "\t" 581 continue 582 endif 583 endif 584 let result .= a:quotedText[i] 585 let i += 1 586 endwhile 587 return result 588endfunc 589 590" Extract the "name" value from a gdb message with fullname="name". 591func s:GetFullname(msg) 592 if a:msg !~ 'fullname' 593 return '' 594 endif 595 let name = s:DecodeMessage(substitute(a:msg, '.*fullname=', '', '')) 596 if has('win32') && name =~ ':\\\\' 597 " sometimes the name arrives double-escaped 598 let name = substitute(name, '\\\\', '\\', 'g') 599 endif 600 return name 601endfunc 602 603" Extract the "addr" value from a gdb message with addr="0x0001234". 604func s:GetAsmAddr(msg) 605 if a:msg !~ 'addr=' 606 return '' 607 endif 608 let addr = s:DecodeMessage(substitute(a:msg, '.*addr=', '', '')) 609 return addr 610endfunc 611 612func s:EndTermDebug(job, status) 613 if exists('#User#TermdebugStopPre') 614 doauto <nomodeline> User TermdebugStopPre 615 endif 616 617 exe 'bwipe! ' . s:commbuf 618 unlet s:gdbwin 619 620 call s:EndDebugCommon() 621endfunc 622 623func s:EndDebugCommon() 624 let curwinid = win_getid(winnr()) 625 626 if exists('s:ptybuf') && s:ptybuf 627 exe 'bwipe! ' . s:ptybuf 628 endif 629 630 " Restore 'signcolumn' in all buffers for which it was set. 631 call win_gotoid(s:sourcewin) 632 let was_buf = bufnr() 633 for bufnr in s:signcolumn_buflist 634 if bufexists(bufnr) 635 exe bufnr .. "buf" 636 if exists('b:save_signcolumn') 637 let &signcolumn = b:save_signcolumn 638 unlet b:save_signcolumn 639 endif 640 endif 641 endfor 642 exe was_buf .. "buf" 643 644 call s:DeleteCommands() 645 646 call win_gotoid(curwinid) 647 648 if s:save_columns > 0 649 let &columns = s:save_columns 650 endif 651 652 if has("balloon_eval") || has("balloon_eval_term") 653 set balloonexpr= 654 if has("balloon_eval") 655 set noballooneval 656 endif 657 if has("balloon_eval_term") 658 set noballoonevalterm 659 endif 660 endif 661 662 if exists('#User#TermdebugStopPost') 663 doauto <nomodeline> User TermdebugStopPost 664 endif 665 666 au! TermDebug 667endfunc 668 669func s:EndPromptDebug(job, status) 670 if exists('#User#TermdebugStopPre') 671 doauto <nomodeline> User TermdebugStopPre 672 endif 673 674 let curwinid = win_getid(winnr()) 675 call win_gotoid(s:gdbwin) 676 set nomodified 677 close 678 if curwinid != s:gdbwin 679 call win_gotoid(curwinid) 680 endif 681 682 call s:EndDebugCommon() 683 unlet s:gdbwin 684 call ch_log("Returning from EndPromptDebug()") 685endfunc 686 687" Disassembly window - added by Michael Sartain 688" 689" - CommOutput: disassemble $pc 690" - CommOutput: &"disassemble $pc\n" 691" - CommOutput: ~"Dump of assembler code for function main(int, char**):\n" 692" - CommOutput: ~" 0x0000555556466f69 <+0>:\tpush rbp\n" 693" ... 694" - CommOutput: ~" 0x0000555556467cd0:\tpop rbp\n" 695" - CommOutput: ~" 0x0000555556467cd1:\tret \n" 696" - CommOutput: ~"End of assembler dump.\n" 697" - CommOutput: ^done 698 699" - CommOutput: disassemble $pc 700" - CommOutput: &"disassemble $pc\n" 701" - CommOutput: &"No function contains specified address.\n" 702" - CommOutput: ^error,msg="No function contains specified address." 703func s:HandleDisasmMsg(msg) 704 if a:msg =~ '^\^done' 705 let curwinid = win_getid(winnr()) 706 if win_gotoid(s:asmwin) 707 silent normal! gg0"_dG 708 call setline(1, s:asm_lines) 709 set nomodified 710 set filetype=asm 711 712 let lnum = search('^' . s:asm_addr) 713 if lnum != 0 714 exe 'sign unplace ' . s:asm_id 715 exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC' 716 endif 717 718 call win_gotoid(curwinid) 719 endif 720 721 let s:parsing_disasm_msg = 0 722 let s:asm_lines = [] 723 elseif a:msg =~ '^\^error,msg=' 724 if s:parsing_disasm_msg == 1 725 " Disassemble call ran into an error. This can happen when gdb can't 726 " find the function frame address, so let's try to disassemble starting 727 " at current PC 728 call s:SendCommand('disassemble $pc,+100') 729 endif 730 let s:parsing_disasm_msg = 0 731 elseif a:msg =~ '\&\"disassemble \$pc' 732 if a:msg =~ '+100' 733 " This is our second disasm attempt 734 let s:parsing_disasm_msg = 2 735 endif 736 else 737 let value = substitute(a:msg, '^\~\"[ ]*', '', '') 738 let value = substitute(value, '^=>[ ]*', '', '') 739 let value = substitute(value, '\\n\" 740$', '', '') 741 let value = substitute(value, '\\n\"$', '', '') 742 let value = substitute(value, ' 743', '', '') 744 let value = substitute(value, '\\t', ' ', 'g') 745 746 if value != '' || !empty(s:asm_lines) 747 call add(s:asm_lines, value) 748 endif 749 endif 750endfunc 751 752" Handle a message received from gdb on the GDB/MI interface. 753func s:CommOutput(chan, msg) 754 let msgs = split(a:msg, "\r") 755 756 for msg in msgs 757 " remove prefixed NL 758 if msg[0] == "\n" 759 let msg = msg[1:] 760 endif 761 762 if s:parsing_disasm_msg 763 call s:HandleDisasmMsg(msg) 764 elseif msg != '' 765 if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)' 766 call s:HandleCursor(msg) 767 elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,' 768 call s:HandleNewBreakpoint(msg) 769 elseif msg =~ '^=breakpoint-deleted,' 770 call s:HandleBreakpointDelete(msg) 771 elseif msg =~ '^=thread-group-started' 772 call s:HandleProgramRun(msg) 773 elseif msg =~ '^\^done,value=' 774 call s:HandleEvaluate(msg) 775 elseif msg =~ '^\^error,msg=' 776 call s:HandleError(msg) 777 elseif msg =~ '^disassemble' 778 let s:parsing_disasm_msg = 1 779 let s:asm_lines = [] 780 endif 781 endif 782 endfor 783endfunc 784 785func s:GotoProgram() 786 if has('win32') 787 if executable('powershell') 788 call system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', s:pid)) 789 endif 790 else 791 call win_gotoid(s:ptywin) 792 endif 793endfunc 794 795" Install commands in the current window to control the debugger. 796func s:InstallCommands() 797 let save_cpo = &cpo 798 set cpo&vim 799 800 command -nargs=? Break call s:SetBreakpoint(<q-args>) 801 command Clear call s:ClearBreakpoint() 802 command Step call s:SendCommand('-exec-step') 803 command Over call s:SendCommand('-exec-next') 804 command Finish call s:SendCommand('-exec-finish') 805 command -nargs=* Run call s:Run(<q-args>) 806 command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>) 807 command Stop call s:SendCommand('-exec-interrupt') 808 809 " using -exec-continue results in CTRL-C in gdb window not working 810 if s:way == 'prompt' 811 command Continue call s:SendCommand('continue') 812 else 813 command Continue call term_sendkeys(s:gdbbuf, "continue\r") 814 endif 815 816 command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>) 817 command Gdb call win_gotoid(s:gdbwin) 818 command Program call s:GotoProgram() 819 command Source call s:GotoSourcewinOrCreateIt() 820 command Asm call s:GotoAsmwinOrCreateIt() 821 command Winbar call s:InstallWinbar() 822 823 if !exists('g:termdebug_map_K') || g:termdebug_map_K 824 let s:k_map_saved = maparg('K', 'n', 0, 1) 825 nnoremap K :Evaluate<CR> 826 endif 827 828 if has('menu') && &mouse != '' 829 call s:InstallWinbar() 830 831 if !exists('g:termdebug_popup') || g:termdebug_popup != 0 832 let s:saved_mousemodel = &mousemodel 833 let &mousemodel = 'popup_setpos' 834 an 1.200 PopUp.-SEP3- <Nop> 835 an 1.210 PopUp.Set\ breakpoint :Break<CR> 836 an 1.220 PopUp.Clear\ breakpoint :Clear<CR> 837 an 1.230 PopUp.Evaluate :Evaluate<CR> 838 endif 839 endif 840 841 let &cpo = save_cpo 842endfunc 843 844let s:winbar_winids = [] 845 846" Install the window toolbar in the current window. 847func s:InstallWinbar() 848 if has('menu') && &mouse != '' 849 nnoremenu WinBar.Step :Step<CR> 850 nnoremenu WinBar.Next :Over<CR> 851 nnoremenu WinBar.Finish :Finish<CR> 852 nnoremenu WinBar.Cont :Continue<CR> 853 nnoremenu WinBar.Stop :Stop<CR> 854 nnoremenu WinBar.Eval :Evaluate<CR> 855 call add(s:winbar_winids, win_getid(winnr())) 856 endif 857endfunc 858 859" Delete installed debugger commands in the current window. 860func s:DeleteCommands() 861 delcommand Break 862 delcommand Clear 863 delcommand Step 864 delcommand Over 865 delcommand Finish 866 delcommand Run 867 delcommand Arguments 868 delcommand Stop 869 delcommand Continue 870 delcommand Evaluate 871 delcommand Gdb 872 delcommand Program 873 delcommand Source 874 delcommand Asm 875 delcommand Winbar 876 877 if exists('s:k_map_saved') 878 if empty(s:k_map_saved) 879 nunmap K 880 else 881 call mapset('n', 0, s:k_map_saved) 882 endif 883 unlet s:k_map_saved 884 endif 885 886 if has('menu') 887 " Remove the WinBar entries from all windows where it was added. 888 let curwinid = win_getid(winnr()) 889 for winid in s:winbar_winids 890 if win_gotoid(winid) 891 aunmenu WinBar.Step 892 aunmenu WinBar.Next 893 aunmenu WinBar.Finish 894 aunmenu WinBar.Cont 895 aunmenu WinBar.Stop 896 aunmenu WinBar.Eval 897 endif 898 endfor 899 call win_gotoid(curwinid) 900 let s:winbar_winids = [] 901 902 if exists('s:saved_mousemodel') 903 let &mousemodel = s:saved_mousemodel 904 unlet s:saved_mousemodel 905 aunmenu PopUp.-SEP3- 906 aunmenu PopUp.Set\ breakpoint 907 aunmenu PopUp.Clear\ breakpoint 908 aunmenu PopUp.Evaluate 909 endif 910 endif 911 912 exe 'sign unplace ' . s:pc_id 913 for [id, entries] in items(s:breakpoints) 914 for subid in keys(entries) 915 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 916 endfor 917 endfor 918 unlet s:breakpoints 919 unlet s:breakpoint_locations 920 921 sign undefine debugPC 922 for val in s:BreakpointSigns 923 exe "sign undefine debugBreakpoint" . val 924 endfor 925 let s:BreakpointSigns = [] 926endfunc 927 928" :Break - Set a breakpoint at the cursor position. 929func s:SetBreakpoint(at) 930 " Setting a breakpoint may not work while the program is running. 931 " Interrupt to make it work. 932 let do_continue = 0 933 if !s:stopped 934 let do_continue = 1 935 if s:way == 'prompt' 936 call s:PromptInterrupt() 937 else 938 call s:SendCommand('-exec-interrupt') 939 endif 940 sleep 10m 941 endif 942 943 " Use the fname:lnum format, older gdb can't handle --source. 944 let at = empty(a:at) ? 945 \ fnameescape(expand('%:p')) . ':' . line('.') : a:at 946 call s:SendCommand('-break-insert ' . at) 947 if do_continue 948 call s:SendCommand('-exec-continue') 949 endif 950endfunc 951 952" :Clear - Delete a breakpoint at the cursor position. 953func s:ClearBreakpoint() 954 let fname = fnameescape(expand('%:p')) 955 let lnum = line('.') 956 let bploc = printf('%s:%d', fname, lnum) 957 if has_key(s:breakpoint_locations, bploc) 958 let idx = 0 959 for id in s:breakpoint_locations[bploc] 960 if has_key(s:breakpoints, id) 961 " Assume this always works, the reply is simply "^done". 962 call s:SendCommand('-break-delete ' . id) 963 for subid in keys(s:breakpoints[id]) 964 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 965 endfor 966 unlet s:breakpoints[id] 967 unlet s:breakpoint_locations[bploc][idx] 968 break 969 else 970 let idx += 1 971 endif 972 endfor 973 if empty(s:breakpoint_locations[bploc]) 974 unlet s:breakpoint_locations[bploc] 975 endif 976 endif 977endfunc 978 979func s:Run(args) 980 if a:args != '' 981 call s:SendCommand('-exec-arguments ' . a:args) 982 endif 983 call s:SendCommand('-exec-run') 984endfunc 985 986func s:SendEval(expr) 987 call s:SendCommand('-data-evaluate-expression "' . a:expr . '"') 988 let s:evalexpr = a:expr 989endfunc 990 991" :Evaluate - evaluate what is under the cursor 992func s:Evaluate(range, arg) 993 if a:arg != '' 994 let expr = a:arg 995 elseif a:range == 2 996 let pos = getcurpos() 997 let reg = getreg('v', 1, 1) 998 let regt = getregtype('v') 999 normal! gv"vy 1000 let expr = @v 1001 call setpos('.', pos) 1002 call setreg('v', reg, regt) 1003 else 1004 let expr = expand('<cexpr>') 1005 endif 1006 let s:ignoreEvalError = 0 1007 call s:SendEval(expr) 1008endfunc 1009 1010let s:ignoreEvalError = 0 1011let s:evalFromBalloonExpr = 0 1012 1013" Handle the result of data-evaluate-expression 1014func s:HandleEvaluate(msg) 1015 let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '') 1016 let value = substitute(value, '\\"', '"', 'g') 1017 if s:evalFromBalloonExpr 1018 if s:evalFromBalloonExprResult == '' 1019 let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value 1020 else 1021 let s:evalFromBalloonExprResult .= ' = ' . value 1022 endif 1023 call balloon_show(s:evalFromBalloonExprResult) 1024 else 1025 echomsg '"' . s:evalexpr . '": ' . value 1026 endif 1027 1028 if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$' 1029 " Looks like a pointer, also display what it points to. 1030 let s:ignoreEvalError = 1 1031 call s:SendEval('*' . s:evalexpr) 1032 else 1033 let s:evalFromBalloonExpr = 0 1034 endif 1035endfunc 1036 1037" Show a balloon with information of the variable under the mouse pointer, 1038" if there is any. 1039func TermDebugBalloonExpr() 1040 if v:beval_winid != s:sourcewin 1041 return '' 1042 endif 1043 if !s:stopped 1044 " Only evaluate when stopped, otherwise setting a breakpoint using the 1045 " mouse triggers a balloon. 1046 return '' 1047 endif 1048 let s:evalFromBalloonExpr = 1 1049 let s:evalFromBalloonExprResult = '' 1050 let s:ignoreEvalError = 1 1051 call s:SendEval(v:beval_text) 1052 return '' 1053endfunc 1054 1055" Handle an error. 1056func s:HandleError(msg) 1057 if s:ignoreEvalError 1058 " Result of s:SendEval() failed, ignore. 1059 let s:ignoreEvalError = 0 1060 let s:evalFromBalloonExpr = 0 1061 return 1062 endif 1063 echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '') 1064endfunc 1065 1066func s:GotoSourcewinOrCreateIt() 1067 if !win_gotoid(s:sourcewin) 1068 new 1069 let s:sourcewin = win_getid(winnr()) 1070 call s:InstallWinbar() 1071 endif 1072endfunc 1073 1074func s:GotoAsmwinOrCreateIt() 1075 if !win_gotoid(s:asmwin) 1076 if win_gotoid(s:sourcewin) 1077 exe 'rightbelow new' 1078 else 1079 exe 'new' 1080 endif 1081 1082 let s:asmwin = win_getid(winnr()) 1083 1084 setlocal nowrap 1085 setlocal number 1086 setlocal noswapfile 1087 setlocal buftype=nofile 1088 1089 let asmbuf = bufnr('Termdebug-asm-listing') 1090 if asmbuf > 0 1091 exe 'buffer' . asmbuf 1092 else 1093 exe 'file Termdebug-asm-listing' 1094 endif 1095 1096 if exists('g:termdebug_disasm_window') 1097 if g:termdebug_disasm_window > 1 1098 exe 'resize ' . g:termdebug_disasm_window 1099 endif 1100 endif 1101 endif 1102 1103 if s:asm_addr != '' 1104 let lnum = search('^' . s:asm_addr) 1105 if lnum == 0 1106 if s:stopped 1107 call s:SendCommand('disassemble $pc') 1108 endif 1109 else 1110 exe 'sign unplace ' . s:asm_id 1111 exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC' 1112 endif 1113 endif 1114endfunc 1115 1116" Handle stopping and running message from gdb. 1117" Will update the sign that shows the current position. 1118func s:HandleCursor(msg) 1119 let wid = win_getid(winnr()) 1120 1121 if a:msg =~ '^\*stopped' 1122 call ch_log('program stopped') 1123 let s:stopped = 1 1124 elseif a:msg =~ '^\*running' 1125 call ch_log('program running') 1126 let s:stopped = 0 1127 endif 1128 1129 if a:msg =~ 'fullname=' 1130 let fname = s:GetFullname(a:msg) 1131 else 1132 let fname = '' 1133 endif 1134 1135 if a:msg =~ 'addr=' 1136 let asm_addr = s:GetAsmAddr(a:msg) 1137 if asm_addr != '' 1138 let s:asm_addr = asm_addr 1139 1140 let curwinid = win_getid(winnr()) 1141 if win_gotoid(s:asmwin) 1142 let lnum = search('^' . s:asm_addr) 1143 if lnum == 0 1144 call s:SendCommand('disassemble $pc') 1145 else 1146 exe 'sign unplace ' . s:asm_id 1147 exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC' 1148 endif 1149 1150 call win_gotoid(curwinid) 1151 endif 1152 endif 1153 endif 1154 1155 if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) 1156 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 1157 if lnum =~ '^[0-9]*$' 1158 call s:GotoSourcewinOrCreateIt() 1159 if expand('%:p') != fnamemodify(fname, ':p') 1160 if &modified 1161 " TODO: find existing window 1162 exe 'split ' . fnameescape(fname) 1163 let s:sourcewin = win_getid(winnr()) 1164 call s:InstallWinbar() 1165 else 1166 exe 'edit ' . fnameescape(fname) 1167 endif 1168 endif 1169 exe lnum 1170 exe 'sign unplace ' . s:pc_id 1171 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC priority=110 file=' . fname 1172 if !exists('b:save_signcolumn') 1173 let b:save_signcolumn = &signcolumn 1174 call add(s:signcolumn_buflist, bufnr()) 1175 endif 1176 setlocal signcolumn=yes 1177 endif 1178 elseif !s:stopped || fname != '' 1179 exe 'sign unplace ' . s:pc_id 1180 endif 1181 1182 call win_gotoid(wid) 1183endfunc 1184 1185let s:BreakpointSigns = [] 1186 1187func s:CreateBreakpoint(id, subid) 1188 let nr = printf('%d.%d', a:id, a:subid) 1189 if index(s:BreakpointSigns, nr) == -1 1190 call add(s:BreakpointSigns, nr) 1191 exe "sign define debugBreakpoint" . nr . " text=" . substitute(nr, '\..*', '', '') . " texthl=debugBreakpoint" 1192 endif 1193endfunc 1194 1195func! s:SplitMsg(s) 1196 return split(a:s, '{.\{-}}\zs') 1197endfunction 1198 1199" Handle setting a breakpoint 1200" Will update the sign that shows the breakpoint 1201func s:HandleNewBreakpoint(msg) 1202 if a:msg !~ 'fullname=' 1203 " a watch does not have a file name 1204 return 1205 endif 1206 for msg in s:SplitMsg(a:msg) 1207 let fname = s:GetFullname(msg) 1208 if empty(fname) 1209 continue 1210 endif 1211 let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '') 1212 if empty(nr) 1213 return 1214 endif 1215 1216 " If "nr" is 123 it becomes "123.0" and subid is "0". 1217 " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded. 1218 let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0') 1219 call s:CreateBreakpoint(id, subid) 1220 1221 if has_key(s:breakpoints, id) 1222 let entries = s:breakpoints[id] 1223 else 1224 let entries = {} 1225 let s:breakpoints[id] = entries 1226 endif 1227 if has_key(entries, subid) 1228 let entry = entries[subid] 1229 else 1230 let entry = {} 1231 let entries[subid] = entry 1232 endif 1233 1234 let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '') 1235 let entry['fname'] = fname 1236 let entry['lnum'] = lnum 1237 1238 let bploc = printf('%s:%d', fname, lnum) 1239 if !has_key(s:breakpoint_locations, bploc) 1240 let s:breakpoint_locations[bploc] = [] 1241 endif 1242 let s:breakpoint_locations[bploc] += [id] 1243 1244 if bufloaded(fname) 1245 call s:PlaceSign(id, subid, entry) 1246 endif 1247 endfor 1248endfunc 1249 1250func s:PlaceSign(id, subid, entry) 1251 let nr = printf('%d.%d', a:id, a:subid) 1252 exe 'sign place ' . s:Breakpoint2SignNumber(a:id, a:subid) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . nr . ' priority=110 file=' . a:entry['fname'] 1253 let a:entry['placed'] = 1 1254endfunc 1255 1256" Handle deleting a breakpoint 1257" Will remove the sign that shows the breakpoint 1258func s:HandleBreakpointDelete(msg) 1259 let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 1260 if empty(id) 1261 return 1262 endif 1263 if has_key(s:breakpoints, id) 1264 for [subid, entry] in items(s:breakpoints[id]) 1265 if has_key(entry, 'placed') 1266 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 1267 unlet entry['placed'] 1268 endif 1269 endfor 1270 unlet s:breakpoints[id] 1271 endif 1272endfunc 1273 1274" Handle the debugged program starting to run. 1275" Will store the process ID in s:pid 1276func s:HandleProgramRun(msg) 1277 let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0 1278 if nr == 0 1279 return 1280 endif 1281 let s:pid = nr 1282 call ch_log('Detected process ID: ' . s:pid) 1283endfunc 1284 1285" Handle a BufRead autocommand event: place any signs. 1286func s:BufRead() 1287 let fname = expand('<afile>:p') 1288 for [id, entries] in items(s:breakpoints) 1289 for [subid, entry] in items(entries) 1290 if entry['fname'] == fname 1291 call s:PlaceSign(id, subid, entry) 1292 endif 1293 endfor 1294 endfor 1295endfunc 1296 1297" Handle a BufUnloaded autocommand event: unplace any signs. 1298func s:BufUnloaded() 1299 let fname = expand('<afile>:p') 1300 for [id, entries] in items(s:breakpoints) 1301 for [subid, entry] in items(entries) 1302 if entry['fname'] == fname 1303 let entry['placed'] = 0 1304 endif 1305 endfor 1306 endfor 1307endfunc 1308 1309let &cpo = s:keepcpo 1310unlet s:keepcpo 1311