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