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