1" Debugger plugin using gdb. 2" 3" Author: Bram Moolenaar 4" Copyright: Vim license applies, see ":help license" 5" Last Change: 2021 Oct 26 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\"\r$', '', '') 740 let value = substitute(value, '\\n\"$', '', '') 741 let value = substitute(value, '\r', '', '') 742 let value = substitute(value, '\\t', ' ', 'g') 743 744 if value != '' || !empty(s:asm_lines) 745 call add(s:asm_lines, value) 746 endif 747 endif 748endfunc 749 750" Handle a message received from gdb on the GDB/MI interface. 751func s:CommOutput(chan, msg) 752 let msgs = split(a:msg, "\r") 753 754 for msg in msgs 755 " remove prefixed NL 756 if msg[0] == "\n" 757 let msg = msg[1:] 758 endif 759 760 if s:parsing_disasm_msg 761 call s:HandleDisasmMsg(msg) 762 elseif msg != '' 763 if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)' 764 call s:HandleCursor(msg) 765 elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,' 766 call s:HandleNewBreakpoint(msg) 767 elseif msg =~ '^=breakpoint-deleted,' 768 call s:HandleBreakpointDelete(msg) 769 elseif msg =~ '^=thread-group-started' 770 call s:HandleProgramRun(msg) 771 elseif msg =~ '^\^done,value=' 772 call s:HandleEvaluate(msg) 773 elseif msg =~ '^\^error,msg=' 774 call s:HandleError(msg) 775 elseif msg =~ '^disassemble' 776 let s:parsing_disasm_msg = 1 777 let s:asm_lines = [] 778 endif 779 endif 780 endfor 781endfunc 782 783func s:GotoProgram() 784 if has('win32') 785 if executable('powershell') 786 call system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', s:pid)) 787 endif 788 else 789 call win_gotoid(s:ptywin) 790 endif 791endfunc 792 793" Install commands in the current window to control the debugger. 794func s:InstallCommands() 795 let save_cpo = &cpo 796 set cpo&vim 797 798 command -nargs=? Break call s:SetBreakpoint(<q-args>) 799 command Clear call s:ClearBreakpoint() 800 command Step call s:SendCommand('-exec-step') 801 command Over call s:SendCommand('-exec-next') 802 command Finish call s:SendCommand('-exec-finish') 803 command -nargs=* Run call s:Run(<q-args>) 804 command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>) 805 command Stop call s:SendCommand('-exec-interrupt') 806 807 " using -exec-continue results in CTRL-C in gdb window not working 808 if s:way == 'prompt' 809 command Continue call s:SendCommand('continue') 810 else 811 command Continue call term_sendkeys(s:gdbbuf, "continue\r") 812 endif 813 814 command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>) 815 command Gdb call win_gotoid(s:gdbwin) 816 command Program call s:GotoProgram() 817 command Source call s:GotoSourcewinOrCreateIt() 818 command Asm call s:GotoAsmwinOrCreateIt() 819 command Winbar call s:InstallWinbar() 820 821 if !exists('g:termdebug_map_K') || g:termdebug_map_K 822 let s:k_map_saved = maparg('K', 'n', 0, 1) 823 nnoremap K :Evaluate<CR> 824 endif 825 826 if has('menu') && &mouse != '' 827 call s:InstallWinbar() 828 829 if !exists('g:termdebug_popup') || g:termdebug_popup != 0 830 let s:saved_mousemodel = &mousemodel 831 let &mousemodel = 'popup_setpos' 832 an 1.200 PopUp.-SEP3- <Nop> 833 an 1.210 PopUp.Set\ breakpoint :Break<CR> 834 an 1.220 PopUp.Clear\ breakpoint :Clear<CR> 835 an 1.230 PopUp.Evaluate :Evaluate<CR> 836 endif 837 endif 838 839 let &cpo = save_cpo 840endfunc 841 842let s:winbar_winids = [] 843 844" Install the window toolbar in the current window. 845func s:InstallWinbar() 846 if has('menu') && &mouse != '' 847 nnoremenu WinBar.Step :Step<CR> 848 nnoremenu WinBar.Next :Over<CR> 849 nnoremenu WinBar.Finish :Finish<CR> 850 nnoremenu WinBar.Cont :Continue<CR> 851 nnoremenu WinBar.Stop :Stop<CR> 852 nnoremenu WinBar.Eval :Evaluate<CR> 853 call add(s:winbar_winids, win_getid(winnr())) 854 endif 855endfunc 856 857" Delete installed debugger commands in the current window. 858func s:DeleteCommands() 859 delcommand Break 860 delcommand Clear 861 delcommand Step 862 delcommand Over 863 delcommand Finish 864 delcommand Run 865 delcommand Arguments 866 delcommand Stop 867 delcommand Continue 868 delcommand Evaluate 869 delcommand Gdb 870 delcommand Program 871 delcommand Source 872 delcommand Asm 873 delcommand Winbar 874 875 if exists('s:k_map_saved') 876 if empty(s:k_map_saved) 877 nunmap K 878 else 879 call mapset('n', 0, s:k_map_saved) 880 endif 881 unlet s:k_map_saved 882 endif 883 884 if has('menu') 885 " Remove the WinBar entries from all windows where it was added. 886 let curwinid = win_getid(winnr()) 887 for winid in s:winbar_winids 888 if win_gotoid(winid) 889 aunmenu WinBar.Step 890 aunmenu WinBar.Next 891 aunmenu WinBar.Finish 892 aunmenu WinBar.Cont 893 aunmenu WinBar.Stop 894 aunmenu WinBar.Eval 895 endif 896 endfor 897 call win_gotoid(curwinid) 898 let s:winbar_winids = [] 899 900 if exists('s:saved_mousemodel') 901 let &mousemodel = s:saved_mousemodel 902 unlet s:saved_mousemodel 903 aunmenu PopUp.-SEP3- 904 aunmenu PopUp.Set\ breakpoint 905 aunmenu PopUp.Clear\ breakpoint 906 aunmenu PopUp.Evaluate 907 endif 908 endif 909 910 exe 'sign unplace ' . s:pc_id 911 for [id, entries] in items(s:breakpoints) 912 for subid in keys(entries) 913 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 914 endfor 915 endfor 916 unlet s:breakpoints 917 unlet s:breakpoint_locations 918 919 sign undefine debugPC 920 for val in s:BreakpointSigns 921 exe "sign undefine debugBreakpoint" . val 922 endfor 923 let s:BreakpointSigns = [] 924endfunc 925 926" :Break - Set a breakpoint at the cursor position. 927func s:SetBreakpoint(at) 928 " Setting a breakpoint may not work while the program is running. 929 " Interrupt to make it work. 930 let do_continue = 0 931 if !s:stopped 932 let do_continue = 1 933 if s:way == 'prompt' 934 call s:PromptInterrupt() 935 else 936 call s:SendCommand('-exec-interrupt') 937 endif 938 sleep 10m 939 endif 940 941 " Use the fname:lnum format, older gdb can't handle --source. 942 let at = empty(a:at) ? 943 \ fnameescape(expand('%:p')) . ':' . line('.') : a:at 944 call s:SendCommand('-break-insert ' . at) 945 if do_continue 946 call s:SendCommand('-exec-continue') 947 endif 948endfunc 949 950" :Clear - Delete a breakpoint at the cursor position. 951func s:ClearBreakpoint() 952 let fname = fnameescape(expand('%:p')) 953 let lnum = line('.') 954 let bploc = printf('%s:%d', fname, lnum) 955 if has_key(s:breakpoint_locations, bploc) 956 let idx = 0 957 for id in s:breakpoint_locations[bploc] 958 if has_key(s:breakpoints, id) 959 " Assume this always works, the reply is simply "^done". 960 call s:SendCommand('-break-delete ' . id) 961 for subid in keys(s:breakpoints[id]) 962 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 963 endfor 964 unlet s:breakpoints[id] 965 unlet s:breakpoint_locations[bploc][idx] 966 break 967 else 968 let idx += 1 969 endif 970 endfor 971 if empty(s:breakpoint_locations[bploc]) 972 unlet s:breakpoint_locations[bploc] 973 endif 974 endif 975endfunc 976 977func s:Run(args) 978 if a:args != '' 979 call s:SendCommand('-exec-arguments ' . a:args) 980 endif 981 call s:SendCommand('-exec-run') 982endfunc 983 984func s:SendEval(expr) 985 " clean up expression that may got in because of range 986 " (newlines and surrounding spaces) 987 let expr = a:expr 988 if &filetype ==# 'cobol' 989 " extra cleanup for COBOL: _every: expression ends with a period, 990 " a trailing comma is ignored as it commonly separates multiple expr. 991 let expr = substitute(expr, '\..*', '', '') 992 let expr = substitute(expr, '[;\n]', ' ', 'g') 993 let expr = substitute(expr, ',*$', '', '') 994 else 995 let expr = substitute(expr, '\n', ' ', 'g') 996 endif 997 let expr = substitute(expr, '^ *\(.*\) *', '\1', '') 998 999 call s:SendCommand('-data-evaluate-expression "' . expr . '"') 1000 let s:evalexpr = expr 1001endfunc 1002 1003" :Evaluate - evaluate what is under the cursor 1004func s:Evaluate(range, arg) 1005 if a:arg != '' 1006 let expr = a:arg 1007 elseif a:range == 2 1008 let pos = getcurpos() 1009 let reg = getreg('v', 1, 1) 1010 let regt = getregtype('v') 1011 normal! gv"vy 1012 let expr = @v 1013 call setpos('.', pos) 1014 call setreg('v', reg, regt) 1015 else 1016 let expr = expand('<cexpr>') 1017 endif 1018 let s:ignoreEvalError = 0 1019 call s:SendEval(expr) 1020endfunc 1021 1022let s:ignoreEvalError = 0 1023let s:evalFromBalloonExpr = 0 1024 1025" Handle the result of data-evaluate-expression 1026func s:HandleEvaluate(msg) 1027 let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '') 1028 let value = substitute(value, '\\"', '"', 'g') 1029 if s:evalFromBalloonExpr 1030 if s:evalFromBalloonExprResult == '' 1031 let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value 1032 else 1033 let s:evalFromBalloonExprResult .= ' = ' . value 1034 endif 1035 call balloon_show(s:evalFromBalloonExprResult) 1036 else 1037 echomsg '"' . s:evalexpr . '": ' . value 1038 endif 1039 1040 if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$' 1041 " Looks like a pointer, also display what it points to. 1042 let s:ignoreEvalError = 1 1043 call s:SendEval('*' . s:evalexpr) 1044 else 1045 let s:evalFromBalloonExpr = 0 1046 endif 1047endfunc 1048 1049" Show a balloon with information of the variable under the mouse pointer, 1050" if there is any. 1051func TermDebugBalloonExpr() 1052 if v:beval_winid != s:sourcewin 1053 return '' 1054 endif 1055 if !s:stopped 1056 " Only evaluate when stopped, otherwise setting a breakpoint using the 1057 " mouse triggers a balloon. 1058 return '' 1059 endif 1060 let s:evalFromBalloonExpr = 1 1061 let s:evalFromBalloonExprResult = '' 1062 let s:ignoreEvalError = 1 1063 call s:SendEval(v:beval_text) 1064 return '' 1065endfunc 1066 1067" Handle an error. 1068func s:HandleError(msg) 1069 if s:ignoreEvalError 1070 " Result of s:SendEval() failed, ignore. 1071 let s:ignoreEvalError = 0 1072 let s:evalFromBalloonExpr = 0 1073 return 1074 endif 1075 let msgVal = substitute(a:msg, '.*msg="\(.*\)"', '\1', '') 1076 echoerr substitute(msgVal, '\\"', '"', 'g') 1077endfunc 1078 1079func s:GotoSourcewinOrCreateIt() 1080 if !win_gotoid(s:sourcewin) 1081 new 1082 let s:sourcewin = win_getid(winnr()) 1083 call s:InstallWinbar() 1084 endif 1085endfunc 1086 1087func s:GotoAsmwinOrCreateIt() 1088 if !win_gotoid(s:asmwin) 1089 if win_gotoid(s:sourcewin) 1090 exe 'rightbelow new' 1091 else 1092 exe 'new' 1093 endif 1094 1095 let s:asmwin = win_getid(winnr()) 1096 1097 setlocal nowrap 1098 setlocal number 1099 setlocal noswapfile 1100 setlocal buftype=nofile 1101 1102 let asmbuf = bufnr('Termdebug-asm-listing') 1103 if asmbuf > 0 1104 exe 'buffer' . asmbuf 1105 else 1106 exe 'file Termdebug-asm-listing' 1107 endif 1108 1109 if exists('g:termdebug_disasm_window') 1110 if g:termdebug_disasm_window > 1 1111 exe 'resize ' . g:termdebug_disasm_window 1112 endif 1113 endif 1114 endif 1115 1116 if s:asm_addr != '' 1117 let lnum = search('^' . s:asm_addr) 1118 if lnum == 0 1119 if s:stopped 1120 call s:SendCommand('disassemble $pc') 1121 endif 1122 else 1123 exe 'sign unplace ' . s:asm_id 1124 exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC' 1125 endif 1126 endif 1127endfunc 1128 1129" Handle stopping and running message from gdb. 1130" Will update the sign that shows the current position. 1131func s:HandleCursor(msg) 1132 let wid = win_getid(winnr()) 1133 1134 if a:msg =~ '^\*stopped' 1135 call ch_log('program stopped') 1136 let s:stopped = 1 1137 elseif a:msg =~ '^\*running' 1138 call ch_log('program running') 1139 let s:stopped = 0 1140 endif 1141 1142 if a:msg =~ 'fullname=' 1143 let fname = s:GetFullname(a:msg) 1144 else 1145 let fname = '' 1146 endif 1147 1148 if a:msg =~ 'addr=' 1149 let asm_addr = s:GetAsmAddr(a:msg) 1150 if asm_addr != '' 1151 let s:asm_addr = asm_addr 1152 1153 let curwinid = win_getid(winnr()) 1154 if win_gotoid(s:asmwin) 1155 let lnum = search('^' . s:asm_addr) 1156 if lnum == 0 1157 call s:SendCommand('disassemble $pc') 1158 else 1159 exe 'sign unplace ' . s:asm_id 1160 exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC' 1161 endif 1162 1163 call win_gotoid(curwinid) 1164 endif 1165 endif 1166 endif 1167 1168 if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) 1169 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 1170 if lnum =~ '^[0-9]*$' 1171 call s:GotoSourcewinOrCreateIt() 1172 if expand('%:p') != fnamemodify(fname, ':p') 1173 if &modified 1174 " TODO: find existing window 1175 exe 'split ' . fnameescape(fname) 1176 let s:sourcewin = win_getid(winnr()) 1177 call s:InstallWinbar() 1178 else 1179 exe 'edit ' . fnameescape(fname) 1180 endif 1181 endif 1182 exe lnum 1183 exe 'sign unplace ' . s:pc_id 1184 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC priority=110 file=' . fname 1185 if !exists('b:save_signcolumn') 1186 let b:save_signcolumn = &signcolumn 1187 call add(s:signcolumn_buflist, bufnr()) 1188 endif 1189 setlocal signcolumn=yes 1190 endif 1191 elseif !s:stopped || fname != '' 1192 exe 'sign unplace ' . s:pc_id 1193 endif 1194 1195 call win_gotoid(wid) 1196endfunc 1197 1198let s:BreakpointSigns = [] 1199 1200func s:CreateBreakpoint(id, subid) 1201 let nr = printf('%d.%d', a:id, a:subid) 1202 if index(s:BreakpointSigns, nr) == -1 1203 call add(s:BreakpointSigns, nr) 1204 exe "sign define debugBreakpoint" . nr . " text=" . substitute(nr, '\..*', '', '') . " texthl=debugBreakpoint" 1205 endif 1206endfunc 1207 1208func! s:SplitMsg(s) 1209 return split(a:s, '{.\{-}}\zs') 1210endfunction 1211 1212" Handle setting a breakpoint 1213" Will update the sign that shows the breakpoint 1214func s:HandleNewBreakpoint(msg) 1215 if a:msg !~ 'fullname=' 1216 " a watch does not have a file name 1217 return 1218 endif 1219 for msg in s:SplitMsg(a:msg) 1220 let fname = s:GetFullname(msg) 1221 if empty(fname) 1222 continue 1223 endif 1224 let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '') 1225 if empty(nr) 1226 return 1227 endif 1228 1229 " If "nr" is 123 it becomes "123.0" and subid is "0". 1230 " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded. 1231 let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0') 1232 call s:CreateBreakpoint(id, subid) 1233 1234 if has_key(s:breakpoints, id) 1235 let entries = s:breakpoints[id] 1236 else 1237 let entries = {} 1238 let s:breakpoints[id] = entries 1239 endif 1240 if has_key(entries, subid) 1241 let entry = entries[subid] 1242 else 1243 let entry = {} 1244 let entries[subid] = entry 1245 endif 1246 1247 let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '') 1248 let entry['fname'] = fname 1249 let entry['lnum'] = lnum 1250 1251 let bploc = printf('%s:%d', fname, lnum) 1252 if !has_key(s:breakpoint_locations, bploc) 1253 let s:breakpoint_locations[bploc] = [] 1254 endif 1255 let s:breakpoint_locations[bploc] += [id] 1256 1257 if bufloaded(fname) 1258 call s:PlaceSign(id, subid, entry) 1259 endif 1260 endfor 1261endfunc 1262 1263func s:PlaceSign(id, subid, entry) 1264 let nr = printf('%d.%d', a:id, a:subid) 1265 exe 'sign place ' . s:Breakpoint2SignNumber(a:id, a:subid) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . nr . ' priority=110 file=' . a:entry['fname'] 1266 let a:entry['placed'] = 1 1267endfunc 1268 1269" Handle deleting a breakpoint 1270" Will remove the sign that shows the breakpoint 1271func s:HandleBreakpointDelete(msg) 1272 let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 1273 if empty(id) 1274 return 1275 endif 1276 if has_key(s:breakpoints, id) 1277 for [subid, entry] in items(s:breakpoints[id]) 1278 if has_key(entry, 'placed') 1279 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 1280 unlet entry['placed'] 1281 endif 1282 endfor 1283 unlet s:breakpoints[id] 1284 endif 1285endfunc 1286 1287" Handle the debugged program starting to run. 1288" Will store the process ID in s:pid 1289func s:HandleProgramRun(msg) 1290 let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0 1291 if nr == 0 1292 return 1293 endif 1294 let s:pid = nr 1295 call ch_log('Detected process ID: ' . s:pid) 1296endfunc 1297 1298" Handle a BufRead autocommand event: place any signs. 1299func s:BufRead() 1300 let fname = expand('<afile>:p') 1301 for [id, entries] in items(s:breakpoints) 1302 for [subid, entry] in items(entries) 1303 if entry['fname'] == fname 1304 call s:PlaceSign(id, subid, entry) 1305 endif 1306 endfor 1307 endfor 1308endfunc 1309 1310" Handle a BufUnloaded autocommand event: unplace any signs. 1311func s:BufUnloaded() 1312 let fname = expand('<afile>:p') 1313 for [id, entries] in items(s:breakpoints) 1314 for [subid, entry] in items(entries) 1315 if entry['fname'] == fname 1316 let entry['placed'] = 0 1317 endif 1318 endfor 1319 endfor 1320endfunc 1321 1322let &cpo = s:keepcpo 1323unlet s:keepcpo 1324