1" Debugger plugin using gdb. 2" 3" Author: Bram Moolenaar 4" Copyright: Vim license applies, see ":help license" 5" Last Change: 2021 Nov 27 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" For neovim compatibility, the vim specific calls were replaced with neovim 38" specific calls: 39" term_start -> term_open 40" term_sendkeys -> chansend 41" term_getline -> getbufline 42" job_info && term_getjob -> using linux command ps to get the tty 43" balloon -> nvim floating window 44" 45" The code for opening the floating window was taken from the beautiful 46" implementation of LanguageClient-Neovim: 47" https://github.com/autozimu/LanguageClient-neovim/blob/0ed9b69dca49c415390a8317b19149f97ae093fa/autoload/LanguageClient.vim#L304 48" 49" Neovim terminal also works seamlessly on windows, which is why the ability 50" Author: Bram Moolenaar 51" Copyright: Vim license applies, see ":help license" 52 53" In case this gets sourced twice. 54if exists(':Termdebug') 55 finish 56endif 57 58" The terminal feature does not work with gdb on win32. 59if !has('win32') 60 let s:way = 'terminal' 61else 62 let s:way = 'prompt' 63endif 64 65let s:keepcpo = &cpo 66set cpo&vim 67 68" The command that starts debugging, e.g. ":Termdebug vim". 69" To end type "quit" in the gdb window. 70command -nargs=* -complete=file -bang Termdebug call s:StartDebug(<bang>0, <f-args>) 71command -nargs=+ -complete=file -bang TermdebugCommand call s:StartDebugCommand(<bang>0, <f-args>) 72 73" Name of the gdb command, defaults to "gdb". 74if !exists('g:termdebugger') 75 let g:termdebugger = 'gdb' 76endif 77 78let s:pc_id = 12 79let s:asm_id = 13 80let s:break_id = 14 " breakpoint number is added to this 81let s:stopped = 1 82 83let s:parsing_disasm_msg = 0 84let s:asm_lines = [] 85let s:asm_addr = '' 86 87" Take a breakpoint number as used by GDB and turn it into an integer. 88" The breakpoint may contain a dot: 123.4 -> 123004 89" The main breakpoint has a zero subid. 90func s:Breakpoint2SignNumber(id, subid) 91 return s:break_id + a:id * 1000 + a:subid 92endfunction 93 94func s:Highlight(init, old, new) 95 let default = a:init ? 'default ' : '' 96 if a:new ==# 'light' && a:old !=# 'light' 97 exe "hi " . default . "debugPC term=reverse ctermbg=lightblue guibg=lightblue" 98 elseif a:new ==# 'dark' && a:old !=# 'dark' 99 exe "hi " . default . "debugPC term=reverse ctermbg=darkblue guibg=darkblue" 100 endif 101endfunc 102 103call s:Highlight(1, '', &background) 104hi default debugBreakpoint term=reverse ctermbg=red guibg=red 105hi default debugBreakpointDisabled term=reverse ctermbg=gray guibg=gray 106 107func s:StartDebug(bang, ...) 108 " First argument is the command to debug, second core file or process ID. 109 call s:StartDebug_internal({'gdb_args': a:000, 'bang': a:bang}) 110endfunc 111 112func s:StartDebugCommand(bang, ...) 113 " First argument is the command to debug, rest are run arguments. 114 call s:StartDebug_internal({'gdb_args': [a:1], 'proc_args': a:000[1:], 'bang': a:bang}) 115endfunc 116 117func s:StartDebug_internal(dict) 118 if exists('s:gdbwin') 119 echoerr 'Terminal debugger already running, cannot run two' 120 return 121 endif 122 if !executable(g:termdebugger) 123 echoerr 'Cannot execute debugger program "' .. g:termdebugger .. '"' 124 return 125 endif 126 127 let s:ptywin = 0 128 let s:pid = 0 129 let s:asmwin = 0 130 131 if exists('#User#TermdebugStartPre') 132 doauto <nomodeline> User TermdebugStartPre 133 endif 134 135 " Uncomment this line to write logging in "debuglog". 136 " call ch_logfile('debuglog', 'w') 137 138 let s:sourcewin = win_getid(winnr()) 139 140 " Remember the old value of 'signcolumn' for each buffer that it's set in, so 141 " that we can restore the value for all buffers. 142 let b:save_signcolumn = &signcolumn 143 let s:signcolumn_buflist = [bufnr()] 144 145 let s:save_columns = 0 146 let s:allleft = 0 147 if exists('g:termdebug_wide') 148 if &columns < g:termdebug_wide 149 let s:save_columns = &columns 150 let &columns = g:termdebug_wide 151 " If we make the Vim window wider, use the whole left halve for the debug 152 " windows. 153 let s:allleft = 1 154 endif 155 let s:vertical = 1 156 else 157 let s:vertical = 0 158 endif 159 160 " Override using a terminal window by setting g:termdebug_use_prompt to 1. 161 let use_prompt = exists('g:termdebug_use_prompt') && g:termdebug_use_prompt 162 if !has('win32') && !use_prompt 163 let s:way = 'terminal' 164 else 165 let s:way = 'prompt' 166 endif 167 168 if s:way == 'prompt' 169 call s:StartDebug_prompt(a:dict) 170 else 171 call s:StartDebug_term(a:dict) 172 endif 173 174 if exists('g:termdebug_disasm_window') 175 if g:termdebug_disasm_window 176 let curwinid = win_getid(winnr()) 177 call s:GotoAsmwinOrCreateIt() 178 call win_gotoid(curwinid) 179 endif 180 endif 181 182 if exists('#User#TermdebugStartPost') 183 doauto <nomodeline> User TermdebugStartPost 184 endif 185endfunc 186 187" Use when debugger didn't start or ended. 188func s:CloseBuffers() 189 exe 'bwipe! ' . s:ptybuf 190 unlet! s:gdbwin 191endfunc 192 193func s:CheckGdbRunning() 194 if nvim_get_chan_info(s:gdb_job_id) == {} 195 echoerr string(g:termdebugger) . ' exited unexpectedly' 196 call s:CloseBuffers() 197 return '' 198 endif 199 return 'ok' 200endfunc 201 202func s:StartDebug_term(dict) 203 " Open a terminal window without a job, to run the debugged program in. 204 execute s:vertical ? 'vnew' : 'new' 205 let s:pty_job_id = termopen('tail -f /dev/null;#gdb program') 206 if s:pty_job_id == 0 207 echoerr 'invalid argument (or job table is full) while opening terminal window' 208 return 209 elseif s:pty_job_id == -1 210 echoerr 'Failed to open the program terminal window' 211 return 212 endif 213 let pty_job_info = nvim_get_chan_info(s:pty_job_id) 214 let s:ptybuf = pty_job_info['buffer'] 215 let pty = pty_job_info['pty'] 216 let s:ptywin = win_getid(winnr()) 217 if s:vertical 218 " Assuming the source code window will get a signcolumn, use two more 219 " columns for that, thus one less for the terminal window. 220 exe (&columns / 2 - 1) . "wincmd |" 221 if s:allleft 222 " use the whole left column 223 wincmd H 224 endif 225 endif 226 227 " Create a hidden terminal window to communicate with gdb 228 let s:comm_job_id = jobstart('tail -f /dev/null;#gdb communication', { 229 \ 'on_stdout': function('s:CommOutput'), 230 \ 'pty': v:true, 231 \ }) 232 " hide terminal buffer 233 if s:comm_job_id == 0 234 echoerr 'invalid argument (or job table is full) while opening communication terminal window' 235 exe 'bwipe! ' . s:ptybuf 236 return 237 elseif s:comm_job_id == -1 238 echoerr 'Failed to open the communication terminal window' 239 exe 'bwipe! ' . s:ptybuf 240 return 241 endif 242 let comm_job_info = nvim_get_chan_info(s:comm_job_id) 243 let commpty = comm_job_info['pty'] 244 245 let gdb_args = get(a:dict, 'gdb_args', []) 246 let proc_args = get(a:dict, 'proc_args', []) 247 248 let gdb_cmd = [g:termdebugger] 249 " Add -quiet to avoid the intro message causing a hit-enter prompt. 250 let gdb_cmd += ['-quiet'] 251 " Disable pagination, it causes everything to stop at the gdb 252 let gdb_cmd += ['-iex', 'set pagination off'] 253 " Interpret commands while the target is running. This should usually only 254 " be exec-interrupt, since many commands don't work properly while the 255 " target is running (so execute during startup). 256 let gdb_cmd += ['-iex', 'set mi-async on'] 257 " Open a terminal window to run the debugger. 258 let gdb_cmd += ['-tty', pty] 259 " Command executed _after_ startup is done, provides us with the necessary feedback 260 let gdb_cmd += ['-ex', 'echo startupdone\n'] 261 262 " Adding arguments requested by the user 263 let gdb_cmd += gdb_args 264 265 execute 'new' 266 " call ch_log('executing "' . join(gdb_cmd) . '"') 267 let s:gdb_job_id = termopen(gdb_cmd, {'on_exit': function('s:EndTermDebug')}) 268 if s:gdb_job_id == 0 269 echoerr 'invalid argument (or job table is full) while opening gdb terminal window' 270 exe 'bwipe! ' . s:ptybuf 271 return 272 elseif s:gdb_job_id == -1 273 echoerr 'Failed to open the gdb terminal window' 274 call s:CloseBuffers() 275 return 276 endif 277 let gdb_job_info = nvim_get_chan_info(s:gdb_job_id) 278 let s:gdbbuf = gdb_job_info['buffer'] 279 let s:gdbwin = win_getid(winnr()) 280 281 " Wait for the "startupdone" message before sending any commands. 282 let try_count = 0 283 while 1 284 if s:CheckGdbRunning() != 'ok' 285 return 286 endif 287 288 for lnum in range(1, 200) 289 if get(getbufline(s:gdbbuf, lnum), 0, '') =~ 'startupdone' 290 let try_count = 9999 291 break 292 endif 293 endfor 294 let try_count += 1 295 if try_count > 300 296 " done or give up after five seconds 297 break 298 endif 299 sleep 10m 300 endwhile 301 302 " Set arguments to be run. 303 if len(proc_args) 304 call chansend(s:gdb_job_id, 'server set args ' . join(proc_args) . "\r") 305 endif 306 307 " Connect gdb to the communication pty, using the GDB/MI interface. 308 " Prefix "server" to avoid adding this to the history. 309 call chansend(s:gdb_job_id, 'server new-ui mi ' . commpty . "\r") 310 311 " Wait for the response to show up, users may not notice the error and wonder 312 " why the debugger doesn't work. 313 let try_count = 0 314 while 1 315 if s:CheckGdbRunning() != 'ok' 316 return 317 endif 318 319 let response = '' 320 for lnum in range(1, 200) 321 let line1 = get(getbufline(s:gdbbuf, lnum), 0, '') 322 let line2 = get(getbufline(s:gdbbuf, lnum + 1), 0, '') 323 if line1 =~ 'new-ui mi ' 324 " response can be in the same line or the next line 325 let response = line1 . line2 326 if response =~ 'Undefined command' 327 echoerr 'Sorry, your gdb is too old, gdb 7.12 is required' 328 " CHECKME: possibly send a "server show version" here 329 call s:CloseBuffers() 330 return 331 endif 332 if response =~ 'New UI allocated' 333 " Success! 334 break 335 endif 336 elseif line1 =~ 'Reading symbols from' && line2 !~ 'new-ui mi ' 337 " Reading symbols might take a while, try more times 338 let try_count -= 1 339 endif 340 endfor 341 if response =~ 'New UI allocated' 342 break 343 endif 344 let try_count += 1 345 if try_count > 100 346 echoerr 'Cannot check if your gdb works, continuing anyway' 347 break 348 endif 349 sleep 10m 350 endwhile 351 352 " Set the filetype, this can be used to add mappings. 353 set filetype=termdebug 354 355 call s:StartDebugCommon(a:dict) 356endfunc 357 358func s:StartDebug_prompt(dict) 359 " Open a window with a prompt buffer to run gdb in. 360 if s:vertical 361 vertical new 362 else 363 new 364 endif 365 let s:gdbwin = win_getid(winnr()) 366 let s:promptbuf = bufnr('') 367 call prompt_setprompt(s:promptbuf, 'gdb> ') 368 set buftype=prompt 369 file gdb 370 call prompt_setcallback(s:promptbuf, function('s:PromptCallback')) 371 call prompt_setinterrupt(s:promptbuf, function('s:PromptInterrupt')) 372 373 if s:vertical 374 " Assuming the source code window will get a signcolumn, use two more 375 " columns for that, thus one less for the terminal window. 376 exe (&columns / 2 - 1) . "wincmd |" 377 endif 378 379 let gdb_args = get(a:dict, 'gdb_args', []) 380 let proc_args = get(a:dict, 'proc_args', []) 381 382 let gdb_cmd = [g:termdebugger] 383 " Add -quiet to avoid the intro message causing a hit-enter prompt. 384 let gdb_cmd += ['-quiet'] 385 " Disable pagination, it causes everything to stop at the gdb, needs to be run early 386 let gdb_cmd += ['-iex', 'set pagination off'] 387 " Interpret commands while the target is running. This should usually only 388 " be exec-interrupt, since many commands don't work properly while the 389 " target is running (so execute during startup). 390 let gdb_cmd += ['-iex', 'set mi-async on'] 391 " directly communicate via mi2 392 let gdb_cmd += ['--interpreter=mi2'] 393 394 " Adding arguments requested by the user 395 let gdb_cmd += gdb_args 396 397 " call ch_log('executing "' . join(gdb_cmd) . '"') 398 let s:gdbjob = jobstart(gdb_cmd, { 399 \ 'on_exit': function('s:EndPromptDebug'), 400 \ 'on_stdout': function('s:GdbOutCallback'), 401 \ }) 402 if s:gdbjob == 0 403 echoerr 'invalid argument (or job table is full) while starting gdb job' 404 exe 'bwipe! ' . s:ptybuf 405 return 406 elseif s:gdbjob == -1 407 echoerr 'Failed to start the gdb job' 408 call s:CloseBuffers() 409 return 410 endif 411 412 let s:ptybuf = 0 413 if has('win32') 414 " MS-Windows: run in a new console window for maximum compatibility 415 call s:SendCommand('set new-console on') 416 else 417 " Unix: Run the debugged program in a terminal window. Open it below the 418 " gdb window. 419 execute 'new' 420 wincmd x | wincmd j 421 belowright let s:pty_job_id = termopen('tail -f /dev/null;#gdb program') 422 if s:pty_job_id == 0 423 echoerr 'invalid argument (or job table is full) while opening terminal window' 424 return 425 elseif s:pty_job_id == -1 426 echoerr 'Failed to open the program terminal window' 427 return 428 endif 429 let pty_job_info = nvim_get_chan_info(s:pty_job_id) 430 let s:ptybuf = pty_job_info['buffer'] 431 let pty = pty_job_info['pty'] 432 let s:ptywin = win_getid(winnr()) 433 call s:SendCommand('tty ' . pty) 434 435 " Since GDB runs in a prompt window, the environment has not been set to 436 " match a terminal window, need to do that now. 437 call s:SendCommand('set env TERM = xterm-color') 438 call s:SendCommand('set env ROWS = ' . winheight(s:ptywin)) 439 call s:SendCommand('set env LINES = ' . winheight(s:ptywin)) 440 call s:SendCommand('set env COLUMNS = ' . winwidth(s:ptywin)) 441 call s:SendCommand('set env COLORS = ' . &t_Co) 442 call s:SendCommand('set env VIM_TERMINAL = ' . v:version) 443 endif 444 call s:SendCommand('set print pretty on') 445 call s:SendCommand('set breakpoint pending on') 446 447 " Set arguments to be run 448 if len(proc_args) 449 call s:SendCommand('set args ' . join(proc_args)) 450 endif 451 452 call s:StartDebugCommon(a:dict) 453 startinsert 454endfunc 455 456func s:StartDebugCommon(dict) 457 " Sign used to highlight the line where the program has stopped. 458 " There can be only one. 459 sign define debugPC linehl=debugPC 460 461 " Install debugger commands in the text window. 462 call win_gotoid(s:sourcewin) 463 call s:InstallCommands() 464 call win_gotoid(s:gdbwin) 465 466 " Contains breakpoints that have been placed, key is a string with the GDB 467 " breakpoint number. 468 " Each entry is a dict, containing the sub-breakpoints. Key is the subid. 469 " For a breakpoint that is just a number the subid is zero. 470 " For a breakpoint "123.4" the id is "123" and subid is "4". 471 " Example, when breakpoint "44", "123", "123.1" and "123.2" exist: 472 " {'44': {'0': entry}, '123': {'0': entry, '1': entry, '2': entry}} 473 let s:breakpoints = {} 474 475 " Contains breakpoints by file/lnum. The key is "fname:lnum". 476 " Each entry is a list of breakpoint IDs at that position. 477 let s:breakpoint_locations = {} 478 479 augroup TermDebug 480 au BufRead * call s:BufRead() 481 au BufUnload * call s:BufUnloaded() 482 au OptionSet background call s:Highlight(0, v:option_old, v:option_new) 483 augroup END 484 485 " Run the command if the bang attribute was given and got to the debug 486 " window. 487 if get(a:dict, 'bang', 0) 488 call s:SendCommand('-exec-run') 489 call win_gotoid(s:ptywin) 490 endif 491endfunc 492 493" Send a command to gdb. "cmd" is the string without line terminator. 494func s:SendCommand(cmd) 495 "call ch_log('sending to gdb: ' . a:cmd) 496 if s:way == 'prompt' 497 call chansend(s:gdbjob, a:cmd . "\n") 498 else 499 call chansend(s:comm_job_id, a:cmd . "\r") 500 endif 501endfunc 502 503" This is global so that a user can create their mappings with this. 504func TermDebugSendCommand(cmd) 505 if s:way == 'prompt' 506 call chansend(s:gdbjob, a:cmd . "\n") 507 else 508 let do_continue = 0 509 if !s:stopped 510 let do_continue = 1 511 if s:way == 'prompt' 512 " Need to send a signal to get the UI to listen. Strangely this is only 513 " needed once. 514 call jobstop(s:gdbjob) 515 else 516 Stop 517 endif 518 sleep 10m 519 endif 520 call chansend(s:gdb_job_id, a:cmd . "\r") 521 if do_continue 522 Continue 523 endif 524 endif 525endfunc 526 527" Send a command only when stopped. Used for :Next and :Step. 528func s:SendCommandIfStopped(cmd) 529 if s:stopped 530 call s:SendCommand(a:cmd) 531 " else 532 " call ch_log('dropping command, program is running: ' . a:cmd) 533 endif 534endfunc 535 536" Function called when entering a line in the prompt buffer. 537func s:PromptCallback(text) 538 call s:SendCommand(a:text) 539endfunc 540 541" Function called when pressing CTRL-C in the prompt buffer and when placing a 542" breakpoint. 543func s:PromptInterrupt() 544 " call ch_log('Interrupting gdb') 545 if has('win32') 546 " Using job_stop() does not work on MS-Windows, need to send SIGTRAP to 547 " the debugger program so that gdb responds again. 548 if s:pid == 0 549 echoerr 'Cannot interrupt gdb, did not find a process ID' 550 else 551 call debugbreak(s:pid) 552 endif 553 else 554 call jobstop(s:gdbjob) 555 endif 556endfunc 557 558" Function called when gdb outputs text. 559func s:GdbOutCallback(job_id, msgs, event) 560 "call ch_log('received from gdb: ' . a:text) 561 562 " Drop the gdb prompt, we have our own. 563 " Drop status and echo'd commands. 564 call filter(a:msgs, { index, val -> 565 \ val !=# '(gdb)' && val !=# '^done' && val[0] !=# '&'}) 566 567 let lines = [] 568 let index = 0 569 570 for msg in a:msgs 571 if msg =~ '^\^error,msg=' 572 if exists('s:evalexpr') 573 \ && s:DecodeMessage(msg[11:]) 574 \ =~ 'A syntax error in expression, near\|No symbol .* in current context' 575 " Silently drop evaluation errors. 576 call remove(a:msgs, index) 577 unlet s:evalexpr 578 continue 579 endif 580 elseif msg[0] == '~' 581 call add(lines, s:DecodeMessage(msg[1:])) 582 call remove(a:msgs, index) 583 continue 584 endif 585 let index += 1 586 endfor 587 588 let curwinid = win_getid(winnr()) 589 call win_gotoid(s:gdbwin) 590 591 " Add the output above the current prompt. 592 for line in lines 593 call append(line('$') - 1, line) 594 endfor 595 if !empty(lines) 596 set modified 597 endif 598 599 call win_gotoid(curwinid) 600 call s:CommOutput(a:job_id, a:msgs, a:event) 601endfunc 602 603" Decode a message from gdb. quotedText starts with a ", return the text up 604" to the next ", unescaping characters: 605" - remove line breaks 606" - change \\t to \t 607" - change \0xhh to \xhh 608" - change \ooo to octal 609" - change \\ to \ 610func s:DecodeMessage(quotedText) 611 if a:quotedText[0] != '"' 612 echoerr 'DecodeMessage(): missing quote in ' . a:quotedText 613 return 614 endif 615 return a:quotedText 616 \->substitute('^"\|".*\|\\n', '', 'g') 617 \->substitute('\\t', "\t", 'g') 618 \->substitute('\\0x\(\x\x\)', {-> eval('"\x' .. submatch(1) .. '"')}, 'g') 619 \->substitute('\\\o\o\o', {-> eval('"' .. submatch(0) .. '"')}, 'g') 620 \->substitute('\\\\', '\', 'g') 621endfunc 622 623" Extract the "name" value from a gdb message with fullname="name". 624func s:GetFullname(msg) 625 if a:msg !~ 'fullname' 626 return '' 627 endif 628 let name = s:DecodeMessage(substitute(a:msg, '.*fullname=', '', '')) 629 if has('win32') && name =~ ':\\\\' 630 " sometimes the name arrives double-escaped 631 let name = substitute(name, '\\\\', '\\', 'g') 632 endif 633 return name 634endfunc 635 636" Extract the "addr" value from a gdb message with addr="0x0001234". 637func s:GetAsmAddr(msg) 638 if a:msg !~ 'addr=' 639 return '' 640 endif 641 let addr = s:DecodeMessage(substitute(a:msg, '.*addr=', '', '')) 642 return addr 643endfunc 644 645function s:EndTermDebug(job_id, exit_code, event) 646 if exists('#User#TermdebugStopPre') 647 doauto <nomodeline> User TermdebugStopPre 648 endif 649 650 unlet s:gdbwin 651 652 call s:EndDebugCommon() 653endfunc 654 655func s:EndDebugCommon() 656 let curwinid = win_getid(winnr()) 657 658 if exists('s:ptybuf') && s:ptybuf 659 exe 'bwipe! ' . s:ptybuf 660 endif 661 662 " Restore 'signcolumn' in all buffers for which it was set. 663 call win_gotoid(s:sourcewin) 664 let was_buf = bufnr() 665 for bufnr in s:signcolumn_buflist 666 if bufexists(bufnr) 667 exe bufnr .. "buf" 668 if exists('b:save_signcolumn') 669 let &signcolumn = b:save_signcolumn 670 unlet b:save_signcolumn 671 endif 672 endif 673 endfor 674 exe was_buf .. "buf" 675 676 call s:DeleteCommands() 677 678 call win_gotoid(curwinid) 679 680 if s:save_columns > 0 681 let &columns = s:save_columns 682 endif 683 684 if exists('#User#TermdebugStopPost') 685 doauto <nomodeline> User TermdebugStopPost 686 endif 687 688 au! TermDebug 689endfunc 690 691func s:EndPromptDebug(job_id, exit_code, event) 692 if exists('#User#TermdebugStopPre') 693 doauto <nomodeline> User TermdebugStopPre 694 endif 695 696 let curwinid = win_getid(winnr()) 697 call win_gotoid(s:gdbwin) 698 close 699 if curwinid != s:gdbwin 700 call win_gotoid(curwinid) 701 endif 702 703 call s:EndDebugCommon() 704 unlet s:gdbwin 705 "call ch_log("Returning from EndPromptDebug()") 706endfunc 707 708" - CommOutput: disassemble $pc 709" - CommOutput: &"disassemble $pc\n" 710" - CommOutput: ~"Dump of assembler code for function main(int, char**):\n" 711" - CommOutput: ~" 0x0000555556466f69 <+0>:\tpush rbp\n" 712" ... 713" - CommOutput: ~" 0x0000555556467cd0:\tpop rbp\n" 714" - CommOutput: ~" 0x0000555556467cd1:\tret \n" 715" - CommOutput: ~"End of assembler dump.\n" 716" - CommOutput: ^done 717 718" - CommOutput: disassemble $pc 719" - CommOutput: &"disassemble $pc\n" 720" - CommOutput: &"No function contains specified address.\n" 721" - CommOutput: ^error,msg="No function contains specified address." 722func s:HandleDisasmMsg(msg) 723 if a:msg =~ '^\^done' 724 let curwinid = win_getid(winnr()) 725 if win_gotoid(s:asmwin) 726 silent normal! gg0"_dG 727 call setline(1, s:asm_lines) 728 set nomodified 729 set filetype=asm 730 731 let lnum = search('^' . s:asm_addr) 732 if lnum != 0 733 exe 'sign unplace ' . s:asm_id 734 exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC' 735 endif 736 737 call win_gotoid(curwinid) 738 endif 739 740 let s:parsing_disasm_msg = 0 741 let s:asm_lines = [] 742 elseif a:msg =~ '^\^error,msg=' 743 if s:parsing_disasm_msg == 1 744 " Disassemble call ran into an error. This can happen when gdb can't 745 " find the function frame address, so let's try to disassemble starting 746 " at current PC 747 call s:SendCommand('disassemble $pc,+100') 748 endif 749 let s:parsing_disasm_msg = 0 750 elseif a:msg =~ '\&\"disassemble \$pc' 751 if a:msg =~ '+100' 752 " This is our second disasm attempt 753 let s:parsing_disasm_msg = 2 754 endif 755 else 756 let value = substitute(a:msg, '^\~\"[ ]*', '', '') 757 let value = substitute(value, '^=>[ ]*', '', '') 758 let value = substitute(value, '\\n\"\r$', '', '') 759 let value = substitute(value, '\r', '', '') 760 let value = substitute(value, '\\t', ' ', 'g') 761 762 if value != '' || !empty(s:asm_lines) 763 call add(s:asm_lines, value) 764 endif 765 endif 766endfunc 767 768func s:CommOutput(job_id, msgs, event) 769 770 for msg in a:msgs 771 " remove prefixed NL 772 if msg[0] == "\n" 773 let msg = msg[1:] 774 endif 775 776 if s:parsing_disasm_msg 777 call s:HandleDisasmMsg(msg) 778 elseif msg != '' 779 if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)' 780 call s:HandleCursor(msg) 781 elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,' 782 call s:HandleNewBreakpoint(msg, 0) 783 elseif msg =~ '^=breakpoint-modified,' 784 call s:HandleNewBreakpoint(msg, 1) 785 elseif msg =~ '^=breakpoint-deleted,' 786 call s:HandleBreakpointDelete(msg) 787 elseif msg =~ '^=thread-group-started' 788 call s:HandleProgramRun(msg) 789 elseif msg =~ '^\^done,value=' 790 call s:HandleEvaluate(msg) 791 elseif msg =~ '^\^error,msg=' 792 call s:HandleError(msg) 793 elseif msg =~ '^disassemble' 794 let s:parsing_disasm_msg = 1 795 let s:asm_lines = [] 796 endif 797 endif 798 endfor 799endfunc 800 801func s:GotoProgram() 802 if has('win32') 803 if executable('powershell') 804 call system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', s:pid)) 805 endif 806 else 807 call win_gotoid(s:ptywin) 808 endif 809endfunc 810 811" Install commands in the current window to control the debugger. 812func s:InstallCommands() 813 let save_cpo = &cpo 814 set cpo&vim 815 816 command -nargs=? Break call s:SetBreakpoint(<q-args>) 817 command Clear call s:ClearBreakpoint() 818 command Step call s:SendCommandIfStopped('-exec-step') 819 command Over call s:SendCommandIfStopped('-exec-next') 820 command Finish call s:SendCommandIfStopped('-exec-finish') 821 command -nargs=* Run call s:Run(<q-args>) 822 command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>) 823 824 if s:way == 'prompt' 825 command Stop call s:PromptInterrupt() 826 command Continue call s:SendCommand('continue') 827 else 828 command Stop call s:SendCommand('-exec-interrupt') 829 " using -exec-continue results in CTRL-C in the gdb window not working, 830 " communicating via commbuf (= use of SendCommand) has the same result 831 "command Continue call s:SendCommand('-exec-continue') 832 command Continue call chansend(s:gdb_job_id, "continue\r") 833 endif 834 835 command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>) 836 command Gdb call win_gotoid(s:gdbwin) 837 command Program call s:GotoProgram() 838 command Source call s:GotoSourcewinOrCreateIt() 839 command Asm call s:GotoAsmwinOrCreateIt() 840 command Winbar call s:InstallWinbar() 841 842 if !exists('g:termdebug_map_K') || g:termdebug_map_K 843 " let s:k_map_saved = maparg('K', 'n', 0, 1) 844 let s:k_map_saved = {} 845 for map in nvim_get_keymap('n') 846 if map.lhs ==# 'K' 847 let s:k_map_saved = map 848 break 849 endif 850 endfor 851 nnoremap K :Evaluate<CR> 852 endif 853 854 let &cpo = save_cpo 855endfunc 856 857" let s:winbar_winids = [] 858 859" Install the window toolbar in the current window. 860func s:InstallWinbar() 861 " if has('menu') && &mouse != '' 862 " nnoremenu WinBar.Step :Step<CR> 863 " nnoremenu WinBar.Next :Over<CR> 864 " nnoremenu WinBar.Finish :Finish<CR> 865 " nnoremenu WinBar.Cont :Continue<CR> 866 " nnoremenu WinBar.Stop :Stop<CR> 867 " nnoremenu WinBar.Eval :Evaluate<CR> 868 " call add(s:winbar_winids, win_getid(winnr())) 869 " endif 870endfunc 871 872" Delete installed debugger commands in the current window. 873func s:DeleteCommands() 874 delcommand Break 875 delcommand Clear 876 delcommand Step 877 delcommand Over 878 delcommand Finish 879 delcommand Run 880 delcommand Arguments 881 delcommand Stop 882 delcommand Continue 883 delcommand Evaluate 884 delcommand Gdb 885 delcommand Program 886 delcommand Source 887 delcommand Asm 888 delcommand Winbar 889 890 if exists('s:k_map_saved') 891 if empty(s:k_map_saved) 892 nunmap K 893 else 894 " call mapset('n', 0, s:k_map_saved) 895 let mode = s:k_map_saved.mode !=# ' ' ? s:k_map_saved.mode : '' 896 call nvim_set_keymap(mode, 'K', s:k_map_saved.rhs, { 897 \ 'expr': s:k_map_saved.expr ? v:true : v:false, 898 \ 'noremap': s:k_map_saved.noremap ? v:true : v:false, 899 \ 'nowait': s:k_map_saved.nowait ? v:true : v:false, 900 \ 'script': s:k_map_saved.script ? v:true : v:false, 901 \ 'silent': s:k_map_saved.silent ? v:true : v:false, 902 \ }) 903 endif 904 unlet s:k_map_saved 905 endif 906 907 exe 'sign unplace ' . s:pc_id 908 for [id, entries] in items(s:breakpoints) 909 for subid in keys(entries) 910 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 911 endfor 912 endfor 913 unlet s:breakpoints 914 unlet s:breakpoint_locations 915 916 sign undefine debugPC 917 for val in s:BreakpointSigns 918 exe "sign undefine debugBreakpoint" . val 919 endfor 920 let s:BreakpointSigns = [] 921endfunc 922 923" :Break - Set a breakpoint at the cursor position. 924func s:SetBreakpoint(at) 925 " Setting a breakpoint may not work while the program is running. 926 " Interrupt to make it work. 927 let do_continue = 0 928 if !s:stopped 929 let do_continue = 1 930 Stop 931 sleep 10m 932 endif 933 934 " Use the fname:lnum format, older gdb can't handle --source. 935 let at = empty(a:at) ? 936 \ fnameescape(expand('%:p')) . ':' . line('.') : a:at 937 call s:SendCommand('-break-insert ' . at) 938 if do_continue 939 Continue 940 endif 941endfunc 942 943" :Clear - Delete a breakpoint at the cursor position. 944func s:ClearBreakpoint() 945 let fname = fnameescape(expand('%:p')) 946 let lnum = line('.') 947 let bploc = printf('%s:%d', fname, lnum) 948 if has_key(s:breakpoint_locations, bploc) 949 let idx = 0 950 let nr = 0 951 for id in s:breakpoint_locations[bploc] 952 if has_key(s:breakpoints, id) 953 " Assume this always works, the reply is simply "^done". 954 call s:SendCommand('-break-delete ' . id) 955 for subid in keys(s:breakpoints[id]) 956 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 957 endfor 958 unlet s:breakpoints[id] 959 unlet s:breakpoint_locations[bploc][idx] 960 let nr = id 961 break 962 else 963 let idx += 1 964 endif 965 endfor 966 if nr != 0 967 if empty(s:breakpoint_locations[bploc]) 968 unlet s:breakpoint_locations[bploc] 969 endif 970 echomsg 'Breakpoint ' . id . ' cleared from line ' . lnum . '.' 971 else 972 echoerr 'Internal error trying to remove breakpoint at line ' . lnum . '!' 973 endif 974 else 975 echomsg 'No breakpoint to remove at line ' . lnum . '.' 976 endif 977endfunc 978 979func s:Run(args) 980 if a:args != '' 981 call s:SendCommand('-exec-arguments ' . a:args) 982 endif 983 call s:SendCommand('-exec-run') 984endfunc 985 986func s:SendEval(expr) 987 " check for "likely" boolean expressions, in which case we take it as lhs 988 if a:expr =~ "[=!<>]=" 989 let exprLHS = a:expr 990 else 991 " remove text that is likely an assignment 992 let exprLHS = substitute(a:expr, ' *=.*', '', '') 993 endif 994 995 " encoding expression to prevent bad errors 996 let expr = a:expr 997 let expr = substitute(expr, '\\', '\\\\', 'g') 998 let expr = substitute(expr, '"', '\\"', 'g') 999 call s:SendCommand('-data-evaluate-expression "' . expr . '"') 1000 let s:evalexpr = exprLHS 1001endfunc 1002 1003" :Evaluate - evaluate what is specified / under the cursor 1004func s:Evaluate(range, arg) 1005 let expr = s:GetEvaluationExpression(a:range, a:arg) 1006 let s:ignoreEvalError = 0 1007 call s:SendEval(expr) 1008endfunc 1009 1010" get what is specified / under the cursor 1011func s:GetEvaluationExpression(range, arg) 1012 if a:arg != '' 1013 " user supplied evaluation 1014 let expr = s:CleanupExpr(a:arg) 1015 " DSW: replace "likely copy + paste" assignment 1016 let expr = substitute(expr, '"\([^"]*\)": *', '\1=', 'g') 1017 elseif a:range == 2 1018 let pos = getcurpos() 1019 let reg = getreg('v', 1, 1) 1020 let regt = getregtype('v') 1021 normal! gv"vy 1022 let expr = s:CleanupExpr(@v) 1023 call setpos('.', pos) 1024 call setreg('v', reg, regt) 1025 let s:evalFromBalloonExpr = 1 1026 else 1027 " no evaluation provided: get from C-expression under cursor 1028 " TODO: allow filetype specific lookup #9057 1029 let expr = expand('<cexpr>') 1030 let s:evalFromBalloonExpr = 1 1031 endif 1032 return expr 1033endfunc 1034 1035" clean up expression that may got in because of range 1036" (newlines and surrounding whitespace) 1037" As it can also be specified via ex-command for assignments this function 1038" may not change the "content" parts (like replacing contained spaces 1039func s:CleanupExpr(expr) 1040 " replace all embedded newlines/tabs/... 1041 let expr = substitute(a:expr, '\_s', ' ', 'g') 1042 1043 if &filetype ==# 'cobol' 1044 " extra cleanup for COBOL: 1045 " - a semicolon nmay be used instead of a space 1046 " - a trailing comma or period is ignored as it commonly separates/ends 1047 " multiple expr 1048 let expr = substitute(expr, ';', ' ', 'g') 1049 let expr = substitute(expr, '[,.]\+ *$', '', '') 1050 endif 1051 1052 " get rid of leading and trailing spaces 1053 let expr = substitute(expr, '^ *', '', '') 1054 let expr = substitute(expr, ' *$', '', '') 1055 return expr 1056endfunc 1057 1058let s:ignoreEvalError = 0 1059let s:evalFromBalloonExpr = 0 1060let s:evalFromBalloonExprResult = '' 1061 1062" Handle the result of data-evaluate-expression 1063func s:HandleEvaluate(msg) 1064 let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '') 1065 let value = substitute(value, '\\"', '"', 'g') 1066 " multi-byte characters arrive in octal form 1067 let value = substitute(value, '\\\o\o\o', {-> eval('"' .. submatch(0) .. '"')}, 'g') 1068 let value = substitute(value, ' 1069', '\1', '') 1070 if s:evalFromBalloonExpr 1071 if s:evalFromBalloonExprResult == '' 1072 let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value 1073 else 1074 let s:evalFromBalloonExprResult .= ' = ' . value 1075 endif 1076 let s:evalFromBalloonExprResult = split(s:evalFromBalloonExprResult, '\\n') 1077 call s:OpenHoverPreview(s:evalFromBalloonExprResult, v:null) 1078 let s:evalFromBalloonExprResult = '' 1079 else 1080 echomsg '"' . s:evalexpr . '": ' . value 1081 endif 1082 1083 if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$' 1084 " Looks like a pointer, also display what it points to. 1085 let s:ignoreEvalError = 1 1086 call s:SendEval('*' . s:evalexpr) 1087 else 1088 let s:evalFromBalloonExprResult = '' 1089 endif 1090endfunc 1091 1092function! s:ShouldUseFloatWindow() abort 1093 if exists('*nvim_open_win') && (get(g:, 'termdebug_useFloatingHover', 1) == 1) 1094 return v:true 1095 else 1096 return v:false 1097 endif 1098endfunction 1099 1100function! s:CloseFloatingHoverOnCursorMove(win_id, opened) abort 1101 if getpos('.') == a:opened 1102 " Just after opening floating window, CursorMoved event is run. 1103 " To avoid closing floating window immediately, check the cursor 1104 " was really moved 1105 return 1106 endif 1107 autocmd! nvim_termdebug_close_hover 1108 let winnr = win_id2win(a:win_id) 1109 if winnr == 0 1110 return 1111 endif 1112 call nvim_win_close(a:win_id, v:true) 1113endfunction 1114 1115function! s:CloseFloatingHoverOnBufEnter(win_id, bufnr) abort 1116 let winnr = win_id2win(a:win_id) 1117 if winnr == 0 1118 " Float window was already closed 1119 autocmd! nvim_termdebug_close_hover 1120 return 1121 endif 1122 if winnr == winnr() 1123 " Cursor is moving into floating window. Do not close it 1124 return 1125 endif 1126 if bufnr('%') == a:bufnr 1127 " When current buffer opened hover window, it's not another buffer. Skipped 1128 return 1129 endif 1130 autocmd! nvim_termdebug_close_hover 1131 call nvim_win_close(a:win_id, v:true) 1132 endfunction 1133 1134" Open preview window. Window is open in: 1135" - Floating window on Neovim (0.4.0 or later) 1136" - Preview window on Neovim (0.3.0 or earlier) or Vim 1137function! s:OpenHoverPreview(lines, filetype) abort 1138 " Use local variable since parameter is not modifiable 1139 let lines = a:lines 1140 let bufnr = bufnr('%') 1141 1142 let use_float_win = s:ShouldUseFloatWindow() 1143 if use_float_win 1144 let pos = getpos('.') 1145 1146 " Calculate width and height 1147 let width = 0 1148 for index in range(len(lines)) 1149 let line = lines[index] 1150 let lw = strdisplaywidth(line) 1151 if lw > width 1152 let width = lw 1153 endif 1154 let lines[index] = line 1155 endfor 1156 1157 let height = len(lines) 1158 1159 " Calculate anchor 1160 " Prefer North, but if there is no space, fallback into South 1161 let bottom_line = line('w0') + winheight(0) - 1 1162 if pos[1] + height <= bottom_line 1163 let vert = 'N' 1164 let row = 1 1165 else 1166 let vert = 'S' 1167 let row = 0 1168 endif 1169 1170 " Prefer West, but if there is no space, fallback into East 1171 if pos[2] + width <= &columns 1172 let hor = 'W' 1173 let col = 0 1174 else 1175 let hor = 'E' 1176 let col = 1 1177 endif 1178 1179 let buf = nvim_create_buf(v:false, v:true) 1180 call nvim_buf_set_lines(buf, 0, -1, v:true, lines) 1181 " using v:true for second argument of nvim_open_win make the floating 1182 " window disappear 1183 let float_win_id = nvim_open_win(buf, v:false, { 1184 \ 'relative': 'cursor', 1185 \ 'anchor': vert . hor, 1186 \ 'row': row, 1187 \ 'col': col, 1188 \ 'width': width, 1189 \ 'height': height, 1190 \ 'style': 'minimal', 1191 \ }) 1192 1193 if a:filetype isnot v:null 1194 call nvim_win_set_option(float_win_id, 'filetype', a:filetype) 1195 endif 1196 1197 call nvim_buf_set_option(buf, 'modified', v:false) 1198 call nvim_buf_set_option(buf, 'modifiable', v:false) 1199 1200 " Unlike preview window, :pclose does not close window. Instead, close 1201 " hover window automatically when cursor is moved. 1202 let call_after_move = printf('<SID>CloseFloatingHoverOnCursorMove(%d, %s)', float_win_id, string(pos)) 1203 let call_on_bufenter = printf('<SID>CloseFloatingHoverOnBufEnter(%d, %d)', float_win_id, bufnr) 1204 augroup nvim_termdebug_close_hover 1205 execute 'autocmd CursorMoved,CursorMovedI,InsertEnter <buffer> call ' . call_after_move 1206 execute 'autocmd BufEnter * call ' . call_on_bufenter 1207 augroup END 1208 else 1209 echomsg a:lines[0] 1210 endif 1211endfunction 1212 1213" Handle an error. 1214func s:HandleError(msg) 1215 if s:ignoreEvalError 1216 " Result of s:SendEval() failed, ignore. 1217 let s:ignoreEvalError = 0 1218 let s:evalFromBalloonExpr = 0 1219 return 1220 endif 1221 let msgVal = substitute(a:msg, '.*msg="\(.*\)"', '\1', '') 1222 echoerr substitute(msgVal, '\\"', '"', 'g') 1223endfunc 1224 1225func s:GotoSourcewinOrCreateIt() 1226 if !win_gotoid(s:sourcewin) 1227 new 1228 let s:sourcewin = win_getid(winnr()) 1229 call s:InstallWinbar() 1230 endif 1231endfunc 1232 1233func s:GotoAsmwinOrCreateIt() 1234 if !win_gotoid(s:asmwin) 1235 if win_gotoid(s:sourcewin) 1236 exe 'rightbelow new' 1237 else 1238 exe 'new' 1239 endif 1240 1241 let s:asmwin = win_getid(winnr()) 1242 1243 setlocal nowrap 1244 setlocal number 1245 setlocal noswapfile 1246 setlocal buftype=nofile 1247 setlocal modifiable 1248 1249 let asmbuf = bufnr('Termdebug-asm-listing') 1250 if asmbuf > 0 1251 exe 'buffer' . asmbuf 1252 else 1253 exe 'file Termdebug-asm-listing' 1254 endif 1255 1256 if exists('g:termdebug_disasm_window') 1257 if g:termdebug_disasm_window > 1 1258 exe 'resize ' . g:termdebug_disasm_window 1259 endif 1260 endif 1261 endif 1262 1263 if s:asm_addr != '' 1264 let lnum = search('^' . s:asm_addr) 1265 if lnum == 0 1266 if s:stopped 1267 call s:SendCommand('disassemble $pc') 1268 endif 1269 else 1270 exe 'sign unplace ' . s:asm_id 1271 exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC' 1272 endif 1273 endif 1274endfunc 1275 1276" Handle stopping and running message from gdb. 1277" Will update the sign that shows the current position. 1278func s:HandleCursor(msg) 1279 let wid = win_getid(winnr()) 1280 1281 if a:msg =~ '^\*stopped' 1282 "call ch_log('program stopped') 1283 let s:stopped = 1 1284 elseif a:msg =~ '^\*running' 1285 "call ch_log('program running') 1286 let s:stopped = 0 1287 endif 1288 1289 if a:msg =~ 'fullname=' 1290 let fname = s:GetFullname(a:msg) 1291 else 1292 let fname = '' 1293 endif 1294 1295 if a:msg =~ 'addr=' 1296 let asm_addr = s:GetAsmAddr(a:msg) 1297 if asm_addr != '' 1298 let s:asm_addr = asm_addr 1299 1300 let curwinid = win_getid(winnr()) 1301 if win_gotoid(s:asmwin) 1302 let lnum = search('^' . s:asm_addr) 1303 if lnum == 0 1304 call s:SendCommand('disassemble $pc') 1305 else 1306 exe 'sign unplace ' . s:asm_id 1307 exe 'sign place ' . s:asm_id . ' line=' . lnum . ' name=debugPC' 1308 endif 1309 1310 call win_gotoid(curwinid) 1311 endif 1312 endif 1313 endif 1314 1315 if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) 1316 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 1317 if lnum =~ '^[0-9]*$' 1318 call s:GotoSourcewinOrCreateIt() 1319 if expand('%:p') != fnamemodify(fname, ':p') 1320 if &modified 1321 " TODO: find existing window 1322 exe 'split ' . fnameescape(fname) 1323 let s:sourcewin = win_getid(winnr()) 1324 call s:InstallWinbar() 1325 else 1326 exe 'edit ' . fnameescape(fname) 1327 endif 1328 endif 1329 exe lnum 1330 normal! zv 1331 exe 'sign unplace ' . s:pc_id 1332 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname 1333 if !exists('b:save_signcolumn') 1334 let b:save_signcolumn = &signcolumn 1335 call add(s:signcolumn_buflist, bufnr()) 1336 endif 1337 setlocal signcolumn=yes 1338 endif 1339 elseif !s:stopped || fname != '' 1340 exe 'sign unplace ' . s:pc_id 1341 endif 1342 1343 call win_gotoid(wid) 1344endfunc 1345 1346let s:BreakpointSigns = [] 1347 1348func s:CreateBreakpoint(id, subid, enabled) 1349 let nr = printf('%d.%d', a:id, a:subid) 1350 if index(s:BreakpointSigns, nr) == -1 1351 call add(s:BreakpointSigns, nr) 1352 if a:enabled == "n" 1353 let hiName = "debugBreakpointDisabled" 1354 else 1355 let hiName = "debugBreakpoint" 1356 endif 1357 exe "sign define debugBreakpoint" . nr . " text=" . substitute(nr, '\..*', '', '') . " texthl=" . hiName 1358 endif 1359endfunc 1360 1361func! s:SplitMsg(s) 1362 return split(a:s, '{.\{-}}\zs') 1363endfunction 1364 1365" Handle setting a breakpoint 1366" Will update the sign that shows the breakpoint 1367func s:HandleNewBreakpoint(msg, modifiedFlag) 1368 if a:msg !~ 'fullname=' 1369 " a watch or a pending breakpoint does not have a file name 1370 if a:msg =~ 'pending=' 1371 let nr = substitute(a:msg, '.*number=\"\([0-9.]*\)\".*', '\1', '') 1372 let target = substitute(a:msg, '.*pending=\"\([^"]*\)\".*', '\1', '') 1373 echomsg 'Breakpoint ' . nr . ' (' . target . ') pending.' 1374 endif 1375 return 1376 endif 1377 for msg in s:SplitMsg(a:msg) 1378 let fname = s:GetFullname(msg) 1379 if empty(fname) 1380 continue 1381 endif 1382 let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '') 1383 if empty(nr) 1384 return 1385 endif 1386 1387 " If "nr" is 123 it becomes "123.0" and subid is "0". 1388 " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded. 1389 let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0') 1390 let enabled = substitute(msg, '.*enabled="\([yn]\)".*', '\1', '') 1391 call s:CreateBreakpoint(id, subid, enabled) 1392 1393 if has_key(s:breakpoints, id) 1394 let entries = s:breakpoints[id] 1395 else 1396 let entries = {} 1397 let s:breakpoints[id] = entries 1398 endif 1399 if has_key(entries, subid) 1400 let entry = entries[subid] 1401 else 1402 let entry = {} 1403 let entries[subid] = entry 1404 endif 1405 1406 let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '') 1407 let entry['fname'] = fname 1408 let entry['lnum'] = lnum 1409 1410 let bploc = printf('%s:%d', fname, lnum) 1411 if !has_key(s:breakpoint_locations, bploc) 1412 let s:breakpoint_locations[bploc] = [] 1413 endif 1414 let s:breakpoint_locations[bploc] += [id] 1415 1416 if bufloaded(fname) 1417 call s:PlaceSign(id, subid, entry) 1418 let posMsg = ' at line ' . lnum . '.' 1419 else 1420 let posMsg = ' in ' . fname . ' at line ' . lnum . '.' 1421 endif 1422 if !a:modifiedFlag 1423 let actionTaken = 'created' 1424 elseif enabled == 'n' 1425 let actionTaken = 'disabled' 1426 else 1427 let actionTaken = 'enabled' 1428 endif 1429 echomsg 'Breakpoint ' . nr . ' ' . actionTaken . posMsg 1430 endfor 1431endfunc 1432 1433func s:PlaceSign(id, subid, entry) 1434 let nr = printf('%d.%d', a:id, a:subid) 1435 exe 'sign place ' . s:Breakpoint2SignNumber(a:id, a:subid) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . nr . ' priority=110 file=' . a:entry['fname'] 1436 let a:entry['placed'] = 1 1437endfunc 1438 1439" Handle deleting a breakpoint 1440" Will remove the sign that shows the breakpoint 1441func s:HandleBreakpointDelete(msg) 1442 let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 1443 if empty(id) 1444 return 1445 endif 1446 if has_key(s:breakpoints, id) 1447 for [subid, entry] in items(s:breakpoints[id]) 1448 if has_key(entry, 'placed') 1449 exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid) 1450 unlet entry['placed'] 1451 endif 1452 endfor 1453 unlet s:breakpoints[id] 1454 echomsg 'Breakpoint ' . id . ' cleared.' 1455 endif 1456endfunc 1457 1458" Handle the debugged program starting to run. 1459" Will store the process ID in s:pid 1460func s:HandleProgramRun(msg) 1461 let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0 1462 if nr == 0 1463 return 1464 endif 1465 let s:pid = nr 1466 "call ch_log('Detected process ID: ' . s:pid) 1467endfunc 1468 1469" Handle a BufRead autocommand event: place any signs. 1470func s:BufRead() 1471 let fname = expand('<afile>:p') 1472 for [id, entries] in items(s:breakpoints) 1473 for [subid, entry] in items(entries) 1474 if entry['fname'] == fname 1475 call s:PlaceSign(id, subid, entry) 1476 endif 1477 endfor 1478 endfor 1479endfunc 1480 1481" Handle a BufUnloaded autocommand event: unplace any signs. 1482func s:BufUnloaded() 1483 let fname = expand('<afile>:p') 1484 for [id, entries] in items(s:breakpoints) 1485 for [subid, entry] in items(entries) 1486 if entry['fname'] == fname 1487 let entry['placed'] = 0 1488 endif 1489 endfor 1490 endfor 1491endfunc 1492 1493let &cpo = s:keepcpo 1494unlet s:keepcpo 1495