Home | History | Annotate | Download | only in python-vim-lldb
      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 
     37 import lldb
     38 import vim
     39 
     40 import 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
     47 def 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  
     74 def 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 
     85 def 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 
     96 def _cmd(cmd):
     97   vim.command("call confirm('%s')" % cmd)
     98   vim.command(cmd)
     99 
    100 def 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 
    106 def winnr():
    107   """ Returns currently selected window number """
    108   return int(vim.eval("winnr()"))
    109 
    110 def bufwinnr(name):
    111   """ Returns window number corresponding with buffer name """
    112   return int(vim.eval("bufwinnr('%s')" % name))
    113 
    114 def goto_window(nr):
    115   """ go to window number nr"""
    116   if nr != winnr():
    117     vim.command(str(nr) + ' wincmd w')
    118 
    119 def goto_next_window():
    120   """ go to next window. """
    121   vim.command('wincmd w')
    122   return (winnr(), vim.current.buffer.name)
    123 
    124 def goto_previous_window():
    125   """ go to previously selected window """
    126   vim.command("execute \"normal \\<c-w>p\"")
    127 
    128 def 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 
    132 class 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 
    209 class 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 
    377 class 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 
    446 class 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 
    474 class 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 
    499 class 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 
    519 class 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 
    566 class 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 
    574 class 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 
    595 class 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 
    615 class 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