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