1# 2# This file contains implementations of the LLDB display panes in VIM 3# 4# The most generic way to define a new window is to inherit from VimPane 5# and to implement: 6# - get_content() - returns a string with the pane contents 7# 8# Optionally, to highlight text, implement: 9# - get_highlights() - returns a map 10# 11# And call: 12# - define_highlight(unique_name, colour) 13# at some point in the constructor. 14# 15# 16# If the pane shows some key-value data that is in the context of a 17# single frame, inherit from FrameKeyValuePane and implement: 18# - get_frame_content(self, SBFrame frame) 19# 20# 21# If the pane presents some information that can be retrieved with 22# a simple LLDB command while the subprocess is stopped, inherit 23# from StoppedCommandPane and call: 24# - self.setCommand(command, command_args) 25# at some point in the constructor. 26# 27# Optionally, you can implement: 28# - get_selected_line() 29# to highlight a selected line and place the cursor there. 30# 31# 32# FIXME: implement WatchlistPane to displayed watched expressions 33# FIXME: define interface for interactive panes, like catching enter 34# presses to change selected frame/thread... 35# 36 37import lldb 38import vim 39 40import sys 41 42# ============================================================== 43# Get the description of an lldb object or None if not available 44# ============================================================== 45 46# Shamelessly copy/pasted from lldbutil.py in the test suite 47def get_description(obj, option=None): 48 """Calls lldb_obj.GetDescription() and returns a string, or None. 49 50 For SBTarget, SBBreakpointLocation, and SBWatchpoint lldb objects, an extra 51 option can be passed in to describe the detailed level of description 52 desired: 53 o lldb.eDescriptionLevelBrief 54 o lldb.eDescriptionLevelFull 55 o lldb.eDescriptionLevelVerbose 56 """ 57 method = getattr(obj, 'GetDescription') 58 if not method: 59 return None 60 tuple = (lldb.SBTarget, lldb.SBBreakpointLocation, lldb.SBWatchpoint) 61 if isinstance(obj, tuple): 62 if option is None: 63 option = lldb.eDescriptionLevelBrief 64 65 stream = lldb.SBStream() 66 if option is None: 67 success = method(stream) 68 else: 69 success = method(stream, option) 70 if not success: 71 return None 72 return stream.GetData() 73 74def get_selected_thread(target): 75 """ Returns a tuple with (thread, error) where thread == None if error occurs """ 76 process = target.GetProcess() 77 if process is None or not process.IsValid(): 78 return (None, VimPane.MSG_NO_PROCESS) 79 80 thread = process.GetSelectedThread() 81 if thread is None or not thread.IsValid(): 82 return (None, VimPane.MSG_NO_THREADS) 83 return (thread, "") 84 85def get_selected_frame(target): 86 """ Returns a tuple with (frame, error) where frame == None if error occurs """ 87 (thread, error) = get_selected_thread(target) 88 if thread is None: 89 return (None, error) 90 91 frame = thread.GetSelectedFrame() 92 if frame is None or not frame.IsValid(): 93 return (None, VimPane.MSG_NO_FRAME) 94 return (frame, "") 95 96def _cmd(cmd): 97 vim.command("call confirm('%s')" % cmd) 98 vim.command(cmd) 99 100def move_cursor(line, col=0): 101 """ moves cursor to specified line and col """ 102 cw = vim.current.window 103 if cw.cursor[0] != line: 104 vim.command("execute \"normal %dgg\"" % line) 105 106def winnr(): 107 """ Returns currently selected window number """ 108 return int(vim.eval("winnr()")) 109 110def bufwinnr(name): 111 """ Returns window number corresponding with buffer name """ 112 return int(vim.eval("bufwinnr('%s')" % name)) 113 114def goto_window(nr): 115 """ go to window number nr""" 116 if nr != winnr(): 117 vim.command(str(nr) + ' wincmd w') 118 119def goto_next_window(): 120 """ go to next window. """ 121 vim.command('wincmd w') 122 return (winnr(), vim.current.buffer.name) 123 124def goto_previous_window(): 125 """ go to previously selected window """ 126 vim.command("execute \"normal \\<c-w>p\"") 127 128def have_gui(): 129 """ Returns True if vim is in a gui (Gvim/MacVim), False otherwise. """ 130 return int(vim.eval("has('gui_running')")) == 1 131 132class PaneLayout(object): 133 """ A container for a (vertical) group layout of VimPanes """ 134 135 def __init__(self): 136 self.panes = {} 137 138 def havePane(self, name): 139 """ Returns true if name is a registered pane, False otherwise """ 140 return name in self.panes 141 142 def prepare(self, panes = []): 143 """ Draw panes on screen. If empty list is provided, show all. """ 144 145 # If we can't select a window contained in the layout, we are doing a first draw 146 first_draw = not self.selectWindow(True) 147 did_first_draw = False 148 149 # Prepare each registered pane 150 for name in self.panes: 151 if name in panes or len(panes) == 0: 152 if first_draw: 153 # First window in layout will be created with :vsp, and closed later 154 vim.command(":vsp") 155 first_draw = False 156 did_first_draw = True 157 self.panes[name].prepare() 158 159 if did_first_draw: 160 # Close the split window 161 vim.command(":q") 162 163 self.selectWindow(False) 164 165 def contains(self, bufferName = None): 166 """ Returns True if window with name bufferName is contained in the layout, False otherwise. 167 If bufferName is None, the currently selected window is checked. 168 """ 169 if not bufferName: 170 bufferName = vim.current.buffer.name 171 172 for p in self.panes: 173 if bufferName is not None and bufferName.endswith(p): 174 return True 175 return False 176 177 def selectWindow(self, select_contained = True): 178 """ Selects a window contained in the layout (if select_contained = True) and returns True. 179 If select_contained = False, a window that is not contained is selected. Returns False 180 if no group windows can be selected. 181 """ 182 if select_contained == self.contains(): 183 # Simple case: we are already selected 184 return True 185 186 # Otherwise, switch to next window until we find a contained window, or reach the first window again. 187 first = winnr() 188 (curnum, curname) = goto_next_window() 189 190 while not select_contained == self.contains(curname) and curnum != first: 191 (curnum, curname) = goto_next_window() 192 193 return self.contains(curname) == select_contained 194 195 def hide(self, panes = []): 196 """ Hide panes specified. If empty list provided, hide all. """ 197 for name in self.panes: 198 if name in panes or len(panes) == 0: 199 self.panes[name].destroy() 200 201 def registerForUpdates(self, p): 202 self.panes[p.name] = p 203 204 def update(self, target, controller): 205 for name in self.panes: 206 self.panes[name].update(target, controller) 207 208 209class VimPane(object): 210 """ A generic base class for a pane that displays stuff """ 211 CHANGED_VALUE_HIGHLIGHT_NAME_GUI = 'ColorColumn' 212 CHANGED_VALUE_HIGHLIGHT_NAME_TERM = 'lldb_changed' 213 CHANGED_VALUE_HIGHLIGHT_COLOUR_TERM = 'darkred' 214 215 SELECTED_HIGHLIGHT_NAME_GUI = 'Cursor' 216 SELECTED_HIGHLIGHT_NAME_TERM = 'lldb_selected' 217 SELECTED_HIGHLIGHT_COLOUR_TERM = 'darkblue' 218 219 MSG_NO_TARGET = "Target does not exist." 220 MSG_NO_PROCESS = "Process does not exist." 221 MSG_NO_THREADS = "No valid threads." 222 MSG_NO_FRAME = "No valid frame." 223 224 # list of defined highlights, so we avoid re-defining them 225 highlightTypes = [] 226 227 def __init__(self, owner, name, open_below=False, height=3): 228 self.owner = owner 229 self.name = name 230 self.buffer = None 231 self.maxHeight = 20 232 self.openBelow = open_below 233 self.height = height 234 self.owner.registerForUpdates(self) 235 236 def isPrepared(self): 237 """ check window is OK """ 238 if self.buffer == None or len(dir(self.buffer)) == 0 or bufwinnr(self.name) == -1: 239 return False 240 return True 241 242 def prepare(self, method = 'new'): 243 """ check window is OK, if not then create """ 244 if not self.isPrepared(): 245 self.create(method) 246 247 def on_create(self): 248 pass 249 250 def destroy(self): 251 """ destroy window """ 252 if self.buffer == None or len(dir(self.buffer)) == 0: 253 return 254 vim.command('bdelete ' + self.name) 255 256 def create(self, method): 257 """ create window """ 258 259 if method != 'edit': 260 belowcmd = "below" if self.openBelow else "" 261 vim.command('silent %s %s %s' % (belowcmd, method, self.name)) 262 else: 263 vim.command('silent %s %s' % (method, self.name)) 264 265 self.window = vim.current.window 266 267 # Set LLDB pane options 268 vim.command("setlocal buftype=nofile") # Don't try to open a file 269 vim.command("setlocal noswapfile") # Don't use a swap file 270 vim.command("set nonumber") # Don't display line numbers 271 #vim.command("set nowrap") # Don't wrap text 272 273 # Save some parameters and reference to buffer 274 self.buffer = vim.current.buffer 275 self.width = int( vim.eval("winwidth(0)") ) 276 self.height = int( vim.eval("winheight(0)") ) 277 278 self.on_create() 279 goto_previous_window() 280 281 def update(self, target, controller): 282 """ updates buffer contents """ 283 self.target = target 284 if not self.isPrepared(): 285 # Window is hidden, or otherwise not ready for an update 286 return 287 288 original_cursor = self.window.cursor 289 290 # Select pane 291 goto_window(bufwinnr(self.name)) 292 293 # Clean and update content, and apply any highlights. 294 self.clean() 295 296 if self.write(self.get_content(target, controller)): 297 self.apply_highlights() 298 299 cursor = self.get_selected_line() 300 if cursor is None: 301 # Place the cursor at its original position in the window 302 cursor_line = min(original_cursor[0], len(self.buffer)) 303 cursor_col = min(original_cursor[1], len(self.buffer[cursor_line - 1])) 304 else: 305 # Place the cursor at the location requested by a VimPane implementation 306 cursor_line = min(cursor, len(self.buffer)) 307 cursor_col = self.window.cursor[1] 308 309 self.window.cursor = (cursor_line, cursor_col) 310 311 goto_previous_window() 312 313 def get_selected_line(self): 314 """ Returns the line number to move the cursor to, or None to leave 315 it where the user last left it. 316 Subclasses implement this to define custom behaviour. 317 """ 318 return None 319 320 def apply_highlights(self): 321 """ Highlights each set of lines in each highlight group """ 322 highlights = self.get_highlights() 323 for highlightType in highlights: 324 lines = highlights[highlightType] 325 if len(lines) == 0: 326 continue 327 328 cmd = 'match %s /' % highlightType 329 lines = ['\%' + '%d' % line + 'l' for line in lines] 330 cmd += '\\|'.join(lines) 331 cmd += '/' 332 vim.command(cmd) 333 334 def define_highlight(self, name, colour): 335 """ Defines highlihght """ 336 if name in VimPane.highlightTypes: 337 # highlight already defined 338 return 339 340 vim.command("highlight %s ctermbg=%s guibg=%s" % (name, colour, colour)) 341 VimPane.highlightTypes.append(name) 342 343 def write(self, msg): 344 """ replace buffer with msg""" 345 self.prepare() 346 347 msg = str(msg.encode("utf-8", "replace")).split('\n') 348 try: 349 self.buffer.append(msg) 350 vim.command("execute \"normal ggdd\"") 351 except vim.error: 352 # cannot update window; happens when vim is exiting. 353 return False 354 355 move_cursor(1, 0) 356 return True 357 358 def clean(self): 359 """ clean all datas in buffer """ 360 self.prepare() 361 vim.command(':%d') 362 #self.buffer[:] = None 363 364 def get_content(self, target, controller): 365 """ subclasses implement this to provide pane content """ 366 assert(0 and "pane subclass must implement this") 367 pass 368 369 def get_highlights(self): 370 """ Subclasses implement this to provide pane highlights. 371 This function is expected to return a map of: 372 { highlight_name ==> [line_number, ...], ... } 373 """ 374 return {} 375 376 377class FrameKeyValuePane(VimPane): 378 def __init__(self, owner, name, open_below): 379 """ Initialize parent, define member variables, choose which highlight 380 to use based on whether or not we have a gui (MacVim/Gvim). 381 """ 382 383 VimPane.__init__(self, owner, name, open_below) 384 385 # Map-of-maps key/value history { frame --> { variable_name, variable_value } } 386 self.frameValues = {} 387 388 if have_gui(): 389 self.changedHighlight = VimPane.CHANGED_VALUE_HIGHLIGHT_NAME_GUI 390 else: 391 self.changedHighlight = VimPane.CHANGED_VALUE_HIGHLIGHT_NAME_TERM 392 self.define_highlight(VimPane.CHANGED_VALUE_HIGHLIGHT_NAME_TERM, 393 VimPane.CHANGED_VALUE_HIGHLIGHT_COLOUR_TERM) 394 395 def format_pair(self, key, value, changed = False): 396 """ Formats a key/value pair. Appends a '*' if changed == True """ 397 marker = '*' if changed else ' ' 398 return "%s %s = %s\n" % (marker, key, value) 399 400 def get_content(self, target, controller): 401 """ Get content for a frame-aware pane. Also builds the list of lines that 402 need highlighting (i.e. changed values.) 403 """ 404 if target is None or not target.IsValid(): 405 return VimPane.MSG_NO_TARGET 406 407 self.changedLines = [] 408 409 (frame, err) = get_selected_frame(target) 410 if frame is None: 411 return err 412 413 output = get_description(frame) 414 lineNum = 1 415 416 # Retrieve the last values displayed for this frame 417 frameId = get_description(frame.GetBlock()) 418 if frameId in self.frameValues: 419 frameOldValues = self.frameValues[frameId] 420 else: 421 frameOldValues = {} 422 423 # Read the frame variables 424 vals = self.get_frame_content(frame) 425 for (key, value) in vals: 426 lineNum += 1 427 if len(frameOldValues) == 0 or (key in frameOldValues and frameOldValues[key] == value): 428 output += self.format_pair(key, value) 429 else: 430 output += self.format_pair(key, value, True) 431 self.changedLines.append(lineNum) 432 433 # Save values as oldValues 434 newValues = {} 435 for (key, value) in vals: 436 newValues[key] = value 437 self.frameValues[frameId] = newValues 438 439 return output 440 441 def get_highlights(self): 442 ret = {} 443 ret[self.changedHighlight] = self.changedLines 444 return ret 445 446class LocalsPane(FrameKeyValuePane): 447 """ Pane that displays local variables """ 448 def __init__(self, owner, name = 'locals'): 449 FrameKeyValuePane.__init__(self, owner, name, open_below=True) 450 451 # FIXME: allow users to customize display of args/locals/statics/scope 452 self.arguments = True 453 self.show_locals = True 454 self.show_statics = True 455 self.show_in_scope_only = True 456 457 def format_variable(self, var): 458 """ Returns a Tuple of strings "(Type) Name", "Value" for SBValue var """ 459 val = var.GetValue() 460 if val is None: 461 # If the value is too big, SBValue.GetValue() returns None; replace with ... 462 val = "..." 463 464 return ("(%s) %s" % (var.GetTypeName(), var.GetName()), "%s" % val) 465 466 def get_frame_content(self, frame): 467 """ Returns list of key-value pairs of local variables in frame """ 468 vals = frame.GetVariables(self.arguments, 469 self.show_locals, 470 self.show_statics, 471 self.show_in_scope_only) 472 return [self.format_variable(x) for x in vals] 473 474class RegistersPane(FrameKeyValuePane): 475 """ Pane that displays the contents of registers """ 476 def __init__(self, owner, name = 'registers'): 477 FrameKeyValuePane.__init__(self, owner, name, open_below=True) 478 479 def format_register(self, reg): 480 """ Returns a tuple of strings ("name", "value") for SBRegister reg. """ 481 name = reg.GetName() 482 val = reg.GetValue() 483 if val is None: 484 val = "..." 485 return (name, val.strip()) 486 487 def get_frame_content(self, frame): 488 """ Returns a list of key-value pairs ("name", "value") of registers in frame """ 489 490 result = [] 491 for register_sets in frame.GetRegisters(): 492 # hack the register group name into the list of registers... 493 result.append((" = = %s =" % register_sets.GetName(), "")) 494 495 for reg in register_sets: 496 result.append(self.format_register(reg)) 497 return result 498 499class CommandPane(VimPane): 500 """ Pane that displays the output of an LLDB command """ 501 def __init__(self, owner, name, open_below, process_required=True): 502 VimPane.__init__(self, owner, name, open_below) 503 self.process_required = process_required 504 505 def setCommand(self, command, args = ""): 506 self.command = command 507 self.args = args 508 509 def get_content(self, target, controller): 510 output = "" 511 if not target: 512 output = VimPane.MSG_NO_TARGET 513 elif self.process_required and not target.GetProcess(): 514 output = VimPane.MSG_NO_PROCESS 515 else: 516 (success, output) = controller.getCommandOutput(self.command, self.args) 517 return output 518 519class StoppedCommandPane(CommandPane): 520 """ Pane that displays the output of an LLDB command when the process is 521 stopped; otherwise displays process status. This class also implements 522 highlighting for a single line (to show a single-line selected entity.) 523 """ 524 def __init__(self, owner, name, open_below): 525 """ Initialize parent and define highlight to use for selected line. """ 526 CommandPane.__init__(self, owner, name, open_below) 527 if have_gui(): 528 self.selectedHighlight = VimPane.SELECTED_HIGHLIGHT_NAME_GUI 529 else: 530 self.selectedHighlight = VimPane.SELECTED_HIGHLIGHT_NAME_TERM 531 self.define_highlight(VimPane.SELECTED_HIGHLIGHT_NAME_TERM, 532 VimPane.SELECTED_HIGHLIGHT_COLOUR_TERM) 533 534 def get_content(self, target, controller): 535 """ Returns the output of a command that relies on the process being stopped. 536 If the process is not in 'stopped' state, the process status is returned. 537 """ 538 output = "" 539 if not target or not target.IsValid(): 540 output = VimPane.MSG_NO_TARGET 541 elif not target.GetProcess() or not target.GetProcess().IsValid(): 542 output = VimPane.MSG_NO_PROCESS 543 elif target.GetProcess().GetState() == lldb.eStateStopped: 544 (success, output) = controller.getCommandOutput(self.command, self.args) 545 else: 546 (success, output) = controller.getCommandOutput("process", "status") 547 return output 548 549 def get_highlights(self): 550 """ Highlight the line under the cursor. Users moving the cursor has 551 no effect on the selected line. 552 """ 553 ret = {} 554 line = self.get_selected_line() 555 if line is not None: 556 ret[self.selectedHighlight] = [line] 557 return ret 558 return ret 559 560 def get_selected_line(self): 561 """ Subclasses implement this to control where the cursor (and selected highlight) 562 is placed. 563 """ 564 return None 565 566class DisassemblyPane(CommandPane): 567 """ Pane that displays disassembly around PC """ 568 def __init__(self, owner, name = 'disassembly'): 569 CommandPane.__init__(self, owner, name, open_below=True) 570 571 # FIXME: let users customize the number of instructions to disassemble 572 self.setCommand("disassemble", "-c %d -p" % self.maxHeight) 573 574class ThreadPane(StoppedCommandPane): 575 """ Pane that displays threads list """ 576 def __init__(self, owner, name = 'threads'): 577 StoppedCommandPane.__init__(self, owner, name, open_below=False) 578 self.setCommand("thread", "list") 579 580# FIXME: the function below assumes threads are listed in sequential order, 581# which turns out to not be the case. Highlighting of selected thread 582# will be disabled until this can be fixed. LLDB prints a '*' anyways 583# beside the selected thread, so this is not too big of a problem. 584# def get_selected_line(self): 585# """ Place the cursor on the line with the selected entity. 586# Subclasses should override this to customize selection. 587# Formula: selected_line = selected_thread_id + 1 588# """ 589# (thread, err) = get_selected_thread(self.target) 590# if thread is None: 591# return None 592# else: 593# return thread.GetIndexID() + 1 594 595class BacktracePane(StoppedCommandPane): 596 """ Pane that displays backtrace """ 597 def __init__(self, owner, name = 'backtrace'): 598 StoppedCommandPane.__init__(self, owner, name, open_below=False) 599 self.setCommand("bt", "") 600 601 602 def get_selected_line(self): 603 """ Returns the line number in the buffer with the selected frame. 604 Formula: selected_line = selected_frame_id + 2 605 FIXME: the above formula hack does not work when the function return 606 value is printed in the bt window; the wrong line is highlighted. 607 """ 608 609 (frame, err) = get_selected_frame(self.target) 610 if frame is None: 611 return None 612 else: 613 return frame.GetFrameID() + 2 614 615class BreakpointsPane(CommandPane): 616 def __init__(self, owner, name = 'breakpoints'): 617 super(BreakpointsPane, self).__init__(owner, name, open_below=False, process_required=False) 618 self.setCommand("breakpoint", "list") 619