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