forked from OSchip/llvm-project
670 lines
22 KiB
Python
670 lines
22 KiB
Python
#
|
|
# This file contains implementations of the LLDB display panes in VIM
|
|
#
|
|
# The most generic way to define a new window is to inherit from VimPane
|
|
# and to implement:
|
|
# - get_content() - returns a string with the pane contents
|
|
#
|
|
# Optionally, to highlight text, implement:
|
|
# - get_highlights() - returns a map
|
|
#
|
|
# And call:
|
|
# - define_highlight(unique_name, colour)
|
|
# at some point in the constructor.
|
|
#
|
|
#
|
|
# If the pane shows some key-value data that is in the context of a
|
|
# single frame, inherit from FrameKeyValuePane and implement:
|
|
# - get_frame_content(self, SBFrame frame)
|
|
#
|
|
#
|
|
# If the pane presents some information that can be retrieved with
|
|
# a simple LLDB command while the subprocess is stopped, inherit
|
|
# from StoppedCommandPane and call:
|
|
# - self.setCommand(command, command_args)
|
|
# at some point in the constructor.
|
|
#
|
|
# Optionally, you can implement:
|
|
# - get_selected_line()
|
|
# to highlight a selected line and place the cursor there.
|
|
#
|
|
#
|
|
# FIXME: implement WatchlistPane to displayed watched expressions
|
|
# FIXME: define interface for interactive panes, like catching enter
|
|
# presses to change selected frame/thread...
|
|
#
|
|
|
|
import lldb
|
|
import vim
|
|
|
|
import sys
|
|
|
|
# ==============================================================
|
|
# Get the description of an lldb object or None if not available
|
|
# ==============================================================
|
|
|
|
# Shamelessly copy/pasted from lldbutil.py in the test suite
|
|
|
|
|
|
def get_description(obj, option=None):
|
|
"""Calls lldb_obj.GetDescription() and returns a string, or None.
|
|
|
|
For SBTarget, SBBreakpointLocation, and SBWatchpoint lldb objects, an extra
|
|
option can be passed in to describe the detailed level of description
|
|
desired:
|
|
o lldb.eDescriptionLevelBrief
|
|
o lldb.eDescriptionLevelFull
|
|
o lldb.eDescriptionLevelVerbose
|
|
"""
|
|
method = getattr(obj, 'GetDescription')
|
|
if not method:
|
|
return None
|
|
tuple = (lldb.SBTarget, lldb.SBBreakpointLocation, lldb.SBWatchpoint)
|
|
if isinstance(obj, tuple):
|
|
if option is None:
|
|
option = lldb.eDescriptionLevelBrief
|
|
|
|
stream = lldb.SBStream()
|
|
if option is None:
|
|
success = method(stream)
|
|
else:
|
|
success = method(stream, option)
|
|
if not success:
|
|
return None
|
|
return stream.GetData()
|
|
|
|
|
|
def get_selected_thread(target):
|
|
""" Returns a tuple with (thread, error) where thread == None if error occurs """
|
|
process = target.GetProcess()
|
|
if process is None or not process.IsValid():
|
|
return (None, VimPane.MSG_NO_PROCESS)
|
|
|
|
thread = process.GetSelectedThread()
|
|
if thread is None or not thread.IsValid():
|
|
return (None, VimPane.MSG_NO_THREADS)
|
|
return (thread, "")
|
|
|
|
|
|
def get_selected_frame(target):
|
|
""" Returns a tuple with (frame, error) where frame == None if error occurs """
|
|
(thread, error) = get_selected_thread(target)
|
|
if thread is None:
|
|
return (None, error)
|
|
|
|
frame = thread.GetSelectedFrame()
|
|
if frame is None or not frame.IsValid():
|
|
return (None, VimPane.MSG_NO_FRAME)
|
|
return (frame, "")
|
|
|
|
|
|
def _cmd(cmd):
|
|
vim.command("call confirm('%s')" % cmd)
|
|
vim.command(cmd)
|
|
|
|
|
|
def move_cursor(line, col=0):
|
|
""" moves cursor to specified line and col """
|
|
cw = vim.current.window
|
|
if cw.cursor[0] != line:
|
|
vim.command("execute \"normal %dgg\"" % line)
|
|
|
|
|
|
def winnr():
|
|
""" Returns currently selected window number """
|
|
return int(vim.eval("winnr()"))
|
|
|
|
|
|
def bufwinnr(name):
|
|
""" Returns window number corresponding with buffer name """
|
|
return int(vim.eval("bufwinnr('%s')" % name))
|
|
|
|
|
|
def goto_window(nr):
|
|
""" go to window number nr"""
|
|
if nr != winnr():
|
|
vim.command(str(nr) + ' wincmd w')
|
|
|
|
|
|
def goto_next_window():
|
|
""" go to next window. """
|
|
vim.command('wincmd w')
|
|
return (winnr(), vim.current.buffer.name)
|
|
|
|
|
|
def goto_previous_window():
|
|
""" go to previously selected window """
|
|
vim.command("execute \"normal \\<c-w>p\"")
|
|
|
|
|
|
def have_gui():
|
|
""" Returns True if vim is in a gui (Gvim/MacVim), False otherwise. """
|
|
return int(vim.eval("has('gui_running')")) == 1
|
|
|
|
|
|
class PaneLayout(object):
|
|
""" A container for a (vertical) group layout of VimPanes """
|
|
|
|
def __init__(self):
|
|
self.panes = {}
|
|
|
|
def havePane(self, name):
|
|
""" Returns true if name is a registered pane, False otherwise """
|
|
return name in self.panes
|
|
|
|
def prepare(self, panes=[]):
|
|
""" Draw panes on screen. If empty list is provided, show all. """
|
|
|
|
# If we can't select a window contained in the layout, we are doing a
|
|
# first draw
|
|
first_draw = not self.selectWindow(True)
|
|
did_first_draw = False
|
|
|
|
# Prepare each registered pane
|
|
for name in self.panes:
|
|
if name in panes or len(panes) == 0:
|
|
if first_draw:
|
|
# First window in layout will be created with :vsp, and
|
|
# closed later
|
|
vim.command(":vsp")
|
|
first_draw = False
|
|
did_first_draw = True
|
|
self.panes[name].prepare()
|
|
|
|
if did_first_draw:
|
|
# Close the split window
|
|
vim.command(":q")
|
|
|
|
self.selectWindow(False)
|
|
|
|
def contains(self, bufferName=None):
|
|
""" Returns True if window with name bufferName is contained in the layout, False otherwise.
|
|
If bufferName is None, the currently selected window is checked.
|
|
"""
|
|
if not bufferName:
|
|
bufferName = vim.current.buffer.name
|
|
|
|
for p in self.panes:
|
|
if bufferName is not None and bufferName.endswith(p):
|
|
return True
|
|
return False
|
|
|
|
def selectWindow(self, select_contained=True):
|
|
""" Selects a window contained in the layout (if select_contained = True) and returns True.
|
|
If select_contained = False, a window that is not contained is selected. Returns False
|
|
if no group windows can be selected.
|
|
"""
|
|
if select_contained == self.contains():
|
|
# Simple case: we are already selected
|
|
return True
|
|
|
|
# Otherwise, switch to next window until we find a contained window, or
|
|
# reach the first window again.
|
|
first = winnr()
|
|
(curnum, curname) = goto_next_window()
|
|
|
|
while not select_contained == self.contains(
|
|
curname) and curnum != first:
|
|
(curnum, curname) = goto_next_window()
|
|
|
|
return self.contains(curname) == select_contained
|
|
|
|
def hide(self, panes=[]):
|
|
""" Hide panes specified. If empty list provided, hide all. """
|
|
for name in self.panes:
|
|
if name in panes or len(panes) == 0:
|
|
self.panes[name].destroy()
|
|
|
|
def registerForUpdates(self, p):
|
|
self.panes[p.name] = p
|
|
|
|
def update(self, target, controller):
|
|
for name in self.panes:
|
|
self.panes[name].update(target, controller)
|
|
|
|
|
|
class VimPane(object):
|
|
""" A generic base class for a pane that displays stuff """
|
|
CHANGED_VALUE_HIGHLIGHT_NAME_GUI = 'ColorColumn'
|
|
CHANGED_VALUE_HIGHLIGHT_NAME_TERM = 'lldb_changed'
|
|
CHANGED_VALUE_HIGHLIGHT_COLOUR_TERM = 'darkred'
|
|
|
|
SELECTED_HIGHLIGHT_NAME_GUI = 'Cursor'
|
|
SELECTED_HIGHLIGHT_NAME_TERM = 'lldb_selected'
|
|
SELECTED_HIGHLIGHT_COLOUR_TERM = 'darkblue'
|
|
|
|
MSG_NO_TARGET = "Target does not exist."
|
|
MSG_NO_PROCESS = "Process does not exist."
|
|
MSG_NO_THREADS = "No valid threads."
|
|
MSG_NO_FRAME = "No valid frame."
|
|
|
|
# list of defined highlights, so we avoid re-defining them
|
|
highlightTypes = []
|
|
|
|
def __init__(self, owner, name, open_below=False, height=3):
|
|
self.owner = owner
|
|
self.name = name
|
|
self.buffer = None
|
|
self.maxHeight = 20
|
|
self.openBelow = open_below
|
|
self.height = height
|
|
self.owner.registerForUpdates(self)
|
|
|
|
def isPrepared(self):
|
|
""" check window is OK """
|
|
if self.buffer is None or len(
|
|
dir(self.buffer)) == 0 or bufwinnr(self.name) == -1:
|
|
return False
|
|
return True
|
|
|
|
def prepare(self, method='new'):
|
|
""" check window is OK, if not then create """
|
|
if not self.isPrepared():
|
|
self.create(method)
|
|
|
|
def on_create(self):
|
|
pass
|
|
|
|
def destroy(self):
|
|
""" destroy window """
|
|
if self.buffer is None or len(dir(self.buffer)) == 0:
|
|
return
|
|
vim.command('bdelete ' + self.name)
|
|
|
|
def create(self, method):
|
|
""" create window """
|
|
|
|
if method != 'edit':
|
|
belowcmd = "below" if self.openBelow else ""
|
|
vim.command('silent %s %s %s' % (belowcmd, method, self.name))
|
|
else:
|
|
vim.command('silent %s %s' % (method, self.name))
|
|
|
|
self.window = vim.current.window
|
|
|
|
# Set LLDB pane options
|
|
vim.command("setlocal buftype=nofile") # Don't try to open a file
|
|
vim.command("setlocal noswapfile") # Don't use a swap file
|
|
vim.command("set nonumber") # Don't display line numbers
|
|
# vim.command("set nowrap") # Don't wrap text
|
|
|
|
# Save some parameters and reference to buffer
|
|
self.buffer = vim.current.buffer
|
|
self.width = int(vim.eval("winwidth(0)"))
|
|
self.height = int(vim.eval("winheight(0)"))
|
|
|
|
self.on_create()
|
|
goto_previous_window()
|
|
|
|
def update(self, target, controller):
|
|
""" updates buffer contents """
|
|
self.target = target
|
|
if not self.isPrepared():
|
|
# Window is hidden, or otherwise not ready for an update
|
|
return
|
|
|
|
original_cursor = self.window.cursor
|
|
|
|
# Select pane
|
|
goto_window(bufwinnr(self.name))
|
|
|
|
# Clean and update content, and apply any highlights.
|
|
self.clean()
|
|
|
|
if self.write(self.get_content(target, controller)):
|
|
self.apply_highlights()
|
|
|
|
cursor = self.get_selected_line()
|
|
if cursor is None:
|
|
# Place the cursor at its original position in the window
|
|
cursor_line = min(original_cursor[0], len(self.buffer))
|
|
cursor_col = min(
|
|
original_cursor[1], len(
|
|
self.buffer[
|
|
cursor_line - 1]))
|
|
else:
|
|
# Place the cursor at the location requested by a VimPane
|
|
# implementation
|
|
cursor_line = min(cursor, len(self.buffer))
|
|
cursor_col = self.window.cursor[1]
|
|
|
|
self.window.cursor = (cursor_line, cursor_col)
|
|
|
|
goto_previous_window()
|
|
|
|
def get_selected_line(self):
|
|
""" Returns the line number to move the cursor to, or None to leave
|
|
it where the user last left it.
|
|
Subclasses implement this to define custom behaviour.
|
|
"""
|
|
return None
|
|
|
|
def apply_highlights(self):
|
|
""" Highlights each set of lines in each highlight group """
|
|
highlights = self.get_highlights()
|
|
for highlightType in highlights:
|
|
lines = highlights[highlightType]
|
|
if len(lines) == 0:
|
|
continue
|
|
|
|
cmd = 'match %s /' % highlightType
|
|
lines = ['\%' + '%d' % line + 'l' for line in lines]
|
|
cmd += '\\|'.join(lines)
|
|
cmd += '/'
|
|
vim.command(cmd)
|
|
|
|
def define_highlight(self, name, colour):
|
|
""" Defines highlihght """
|
|
if name in VimPane.highlightTypes:
|
|
# highlight already defined
|
|
return
|
|
|
|
vim.command(
|
|
"highlight %s ctermbg=%s guibg=%s" %
|
|
(name, colour, colour))
|
|
VimPane.highlightTypes.append(name)
|
|
|
|
def write(self, msg):
|
|
""" replace buffer with msg"""
|
|
self.prepare()
|
|
|
|
msg = str(msg.encode("utf-8", "replace")).split('\n')
|
|
try:
|
|
self.buffer.append(msg)
|
|
vim.command("execute \"normal ggdd\"")
|
|
except vim.error:
|
|
# cannot update window; happens when vim is exiting.
|
|
return False
|
|
|
|
move_cursor(1, 0)
|
|
return True
|
|
|
|
def clean(self):
|
|
""" clean all datas in buffer """
|
|
self.prepare()
|
|
vim.command(':%d')
|
|
#self.buffer[:] = None
|
|
|
|
def get_content(self, target, controller):
|
|
""" subclasses implement this to provide pane content """
|
|
assert(0 and "pane subclass must implement this")
|
|
pass
|
|
|
|
def get_highlights(self):
|
|
""" Subclasses implement this to provide pane highlights.
|
|
This function is expected to return a map of:
|
|
{ highlight_name ==> [line_number, ...], ... }
|
|
"""
|
|
return {}
|
|
|
|
|
|
class FrameKeyValuePane(VimPane):
|
|
|
|
def __init__(self, owner, name, open_below):
|
|
""" Initialize parent, define member variables, choose which highlight
|
|
to use based on whether or not we have a gui (MacVim/Gvim).
|
|
"""
|
|
|
|
VimPane.__init__(self, owner, name, open_below)
|
|
|
|
# Map-of-maps key/value history { frame --> { variable_name,
|
|
# variable_value } }
|
|
self.frameValues = {}
|
|
|
|
if have_gui():
|
|
self.changedHighlight = VimPane.CHANGED_VALUE_HIGHLIGHT_NAME_GUI
|
|
else:
|
|
self.changedHighlight = VimPane.CHANGED_VALUE_HIGHLIGHT_NAME_TERM
|
|
self.define_highlight(VimPane.CHANGED_VALUE_HIGHLIGHT_NAME_TERM,
|
|
VimPane.CHANGED_VALUE_HIGHLIGHT_COLOUR_TERM)
|
|
|
|
def format_pair(self, key, value, changed=False):
|
|
""" Formats a key/value pair. Appends a '*' if changed == True """
|
|
marker = '*' if changed else ' '
|
|
return "%s %s = %s\n" % (marker, key, value)
|
|
|
|
def get_content(self, target, controller):
|
|
""" Get content for a frame-aware pane. Also builds the list of lines that
|
|
need highlighting (i.e. changed values.)
|
|
"""
|
|
if target is None or not target.IsValid():
|
|
return VimPane.MSG_NO_TARGET
|
|
|
|
self.changedLines = []
|
|
|
|
(frame, err) = get_selected_frame(target)
|
|
if frame is None:
|
|
return err
|
|
|
|
output = get_description(frame)
|
|
lineNum = 1
|
|
|
|
# Retrieve the last values displayed for this frame
|
|
frameId = get_description(frame.GetBlock())
|
|
if frameId in self.frameValues:
|
|
frameOldValues = self.frameValues[frameId]
|
|
else:
|
|
frameOldValues = {}
|
|
|
|
# Read the frame variables
|
|
vals = self.get_frame_content(frame)
|
|
for (key, value) in vals:
|
|
lineNum += 1
|
|
if len(frameOldValues) == 0 or (
|
|
key in frameOldValues and frameOldValues[key] == value):
|
|
output += self.format_pair(key, value)
|
|
else:
|
|
output += self.format_pair(key, value, True)
|
|
self.changedLines.append(lineNum)
|
|
|
|
# Save values as oldValues
|
|
newValues = {}
|
|
for (key, value) in vals:
|
|
newValues[key] = value
|
|
self.frameValues[frameId] = newValues
|
|
|
|
return output
|
|
|
|
def get_highlights(self):
|
|
ret = {}
|
|
ret[self.changedHighlight] = self.changedLines
|
|
return ret
|
|
|
|
|
|
class LocalsPane(FrameKeyValuePane):
|
|
""" Pane that displays local variables """
|
|
|
|
def __init__(self, owner, name='locals'):
|
|
FrameKeyValuePane.__init__(self, owner, name, open_below=True)
|
|
|
|
# FIXME: allow users to customize display of args/locals/statics/scope
|
|
self.arguments = True
|
|
self.show_locals = True
|
|
self.show_statics = True
|
|
self.show_in_scope_only = True
|
|
|
|
def format_variable(self, var):
|
|
""" Returns a Tuple of strings "(Type) Name", "Value" for SBValue var """
|
|
val = var.GetValue()
|
|
if val is None:
|
|
# If the value is too big, SBValue.GetValue() returns None; replace
|
|
# with ...
|
|
val = "..."
|
|
|
|
return ("(%s) %s" % (var.GetTypeName(), var.GetName()), "%s" % val)
|
|
|
|
def get_frame_content(self, frame):
|
|
""" Returns list of key-value pairs of local variables in frame """
|
|
vals = frame.GetVariables(self.arguments,
|
|
self.show_locals,
|
|
self.show_statics,
|
|
self.show_in_scope_only)
|
|
return [self.format_variable(x) for x in vals]
|
|
|
|
|
|
class RegistersPane(FrameKeyValuePane):
|
|
""" Pane that displays the contents of registers """
|
|
|
|
def __init__(self, owner, name='registers'):
|
|
FrameKeyValuePane.__init__(self, owner, name, open_below=True)
|
|
|
|
def format_register(self, reg):
|
|
""" Returns a tuple of strings ("name", "value") for SBRegister reg. """
|
|
name = reg.GetName()
|
|
val = reg.GetValue()
|
|
if val is None:
|
|
val = "..."
|
|
return (name, val.strip())
|
|
|
|
def get_frame_content(self, frame):
|
|
""" Returns a list of key-value pairs ("name", "value") of registers in frame """
|
|
|
|
result = []
|
|
for register_sets in frame.GetRegisters():
|
|
# hack the register group name into the list of registers...
|
|
result.append((" = = %s =" % register_sets.GetName(), ""))
|
|
|
|
for reg in register_sets:
|
|
result.append(self.format_register(reg))
|
|
return result
|
|
|
|
|
|
class CommandPane(VimPane):
|
|
""" Pane that displays the output of an LLDB command """
|
|
|
|
def __init__(self, owner, name, open_below, process_required=True):
|
|
VimPane.__init__(self, owner, name, open_below)
|
|
self.process_required = process_required
|
|
|
|
def setCommand(self, command, args=""):
|
|
self.command = command
|
|
self.args = args
|
|
|
|
def get_content(self, target, controller):
|
|
output = ""
|
|
if not target:
|
|
output = VimPane.MSG_NO_TARGET
|
|
elif self.process_required and not target.GetProcess():
|
|
output = VimPane.MSG_NO_PROCESS
|
|
else:
|
|
(success, output) = controller.getCommandOutput(
|
|
self.command, self.args)
|
|
return output
|
|
|
|
|
|
class StoppedCommandPane(CommandPane):
|
|
""" Pane that displays the output of an LLDB command when the process is
|
|
stopped; otherwise displays process status. This class also implements
|
|
highlighting for a single line (to show a single-line selected entity.)
|
|
"""
|
|
|
|
def __init__(self, owner, name, open_below):
|
|
""" Initialize parent and define highlight to use for selected line. """
|
|
CommandPane.__init__(self, owner, name, open_below)
|
|
if have_gui():
|
|
self.selectedHighlight = VimPane.SELECTED_HIGHLIGHT_NAME_GUI
|
|
else:
|
|
self.selectedHighlight = VimPane.SELECTED_HIGHLIGHT_NAME_TERM
|
|
self.define_highlight(VimPane.SELECTED_HIGHLIGHT_NAME_TERM,
|
|
VimPane.SELECTED_HIGHLIGHT_COLOUR_TERM)
|
|
|
|
def get_content(self, target, controller):
|
|
""" Returns the output of a command that relies on the process being stopped.
|
|
If the process is not in 'stopped' state, the process status is returned.
|
|
"""
|
|
output = ""
|
|
if not target or not target.IsValid():
|
|
output = VimPane.MSG_NO_TARGET
|
|
elif not target.GetProcess() or not target.GetProcess().IsValid():
|
|
output = VimPane.MSG_NO_PROCESS
|
|
elif target.GetProcess().GetState() == lldb.eStateStopped:
|
|
(success, output) = controller.getCommandOutput(
|
|
self.command, self.args)
|
|
else:
|
|
(success, output) = controller.getCommandOutput("process", "status")
|
|
return output
|
|
|
|
def get_highlights(self):
|
|
""" Highlight the line under the cursor. Users moving the cursor has
|
|
no effect on the selected line.
|
|
"""
|
|
ret = {}
|
|
line = self.get_selected_line()
|
|
if line is not None:
|
|
ret[self.selectedHighlight] = [line]
|
|
return ret
|
|
return ret
|
|
|
|
def get_selected_line(self):
|
|
""" Subclasses implement this to control where the cursor (and selected highlight)
|
|
is placed.
|
|
"""
|
|
return None
|
|
|
|
|
|
class DisassemblyPane(CommandPane):
|
|
""" Pane that displays disassembly around PC """
|
|
|
|
def __init__(self, owner, name='disassembly'):
|
|
CommandPane.__init__(self, owner, name, open_below=True)
|
|
|
|
# FIXME: let users customize the number of instructions to disassemble
|
|
self.setCommand("disassemble", "-c %d -p" % self.maxHeight)
|
|
|
|
|
|
class ThreadPane(StoppedCommandPane):
|
|
""" Pane that displays threads list """
|
|
|
|
def __init__(self, owner, name='threads'):
|
|
StoppedCommandPane.__init__(self, owner, name, open_below=False)
|
|
self.setCommand("thread", "list")
|
|
|
|
# FIXME: the function below assumes threads are listed in sequential order,
|
|
# which turns out to not be the case. Highlighting of selected thread
|
|
# will be disabled until this can be fixed. LLDB prints a '*' anyways
|
|
# beside the selected thread, so this is not too big of a problem.
|
|
# def get_selected_line(self):
|
|
# """ Place the cursor on the line with the selected entity.
|
|
# Subclasses should override this to customize selection.
|
|
# Formula: selected_line = selected_thread_id + 1
|
|
# """
|
|
# (thread, err) = get_selected_thread(self.target)
|
|
# if thread is None:
|
|
# return None
|
|
# else:
|
|
# return thread.GetIndexID() + 1
|
|
|
|
|
|
class BacktracePane(StoppedCommandPane):
|
|
""" Pane that displays backtrace """
|
|
|
|
def __init__(self, owner, name='backtrace'):
|
|
StoppedCommandPane.__init__(self, owner, name, open_below=False)
|
|
self.setCommand("bt", "")
|
|
|
|
def get_selected_line(self):
|
|
""" Returns the line number in the buffer with the selected frame.
|
|
Formula: selected_line = selected_frame_id + 2
|
|
FIXME: the above formula hack does not work when the function return
|
|
value is printed in the bt window; the wrong line is highlighted.
|
|
"""
|
|
|
|
(frame, err) = get_selected_frame(self.target)
|
|
if frame is None:
|
|
return None
|
|
else:
|
|
return frame.GetFrameID() + 2
|
|
|
|
|
|
class BreakpointsPane(CommandPane):
|
|
|
|
def __init__(self, owner, name='breakpoints'):
|
|
super(
|
|
BreakpointsPane,
|
|
self).__init__(
|
|
owner,
|
|
name,
|
|
open_below=False,
|
|
process_required=False)
|
|
self.setCommand("breakpoint", "list")
|