diff options
Diffstat (limited to 'utils/vim-lldb')
-rw-r--r-- | utils/vim-lldb/README | 59 | ||||
-rw-r--r-- | utils/vim-lldb/doc/lldb.txt | 115 | ||||
-rw-r--r-- | utils/vim-lldb/plugin/lldb.vim | 151 | ||||
-rw-r--r-- | utils/vim-lldb/python-vim-lldb/import_lldb.py | 61 | ||||
-rw-r--r-- | utils/vim-lldb/python-vim-lldb/lldb_controller.py | 384 | ||||
-rw-r--r-- | utils/vim-lldb/python-vim-lldb/plugin.py | 14 | ||||
-rw-r--r-- | utils/vim-lldb/python-vim-lldb/vim_panes.py | 618 | ||||
-rw-r--r-- | utils/vim-lldb/python-vim-lldb/vim_signs.py | 73 | ||||
-rw-r--r-- | utils/vim-lldb/python-vim-lldb/vim_ui.py | 235 |
9 files changed, 1710 insertions, 0 deletions
diff --git a/utils/vim-lldb/README b/utils/vim-lldb/README new file mode 100644 index 000000000000..721054a2c84b --- /dev/null +++ b/utils/vim-lldb/README @@ -0,0 +1,59 @@ + +================= +LLDB Vim Frontend +================= + +Prerequisites +------------- + +This plugin is known to work with the following flavours of Vim: + + * Linux (tested on Ubuntu 12.04/12.10): + * vim/gvim (from vim-gnome package version 7.3) + + * Mac OS X (tested on Mountain Lion) + * Vim command-line (7.3 from Xcode) + * MacVim 7.3 + +To install the plugin, ensure you have + * a working version of lldb on your path, or the environment variable LLDB + pointing to the lldb binary you would like to use. + * a python-enabled vim (check with ":python print 2") + + +Installation +------------ + +1) Install the Vim pathogen plugin (it keeps installed plugins organized): + + https://github.com/tpope/vim-pathogen + + Or, for the impatient: + +mkdir -p ~/.vim/autoload ~/.vim/bundle; \ +curl -Sso ~/.vim/autoload/pathogen.vim \ + https://raw.github.com/tpope/vim-pathogen/master/autoload/pathogen.vim + +2) Symlink (or copy) ~/.vim/bundle/vim-lldb to this directory: + +ln -sf <lldb-dir>/utils/vim-lldb ~/.vim/bundle/vim-lldb + +3) Update your help-tags. Start vim, do: + + :Helptags + +4) Have fun! + + +Usage/Getting Help +------------------ +All LLDB commands (with tab-completion) can be accessed in Vim's +command mode. Try it out by typing: + +:L<tab> + +There are several sources of help available: + +:help lldb -- Documentation for this plugin +:Lhelp -- LLDB's built-in help system (i.e lldb 'help' command) +:Lscript help (lldb) -- Complete LLDB Python API reference diff --git a/utils/vim-lldb/doc/lldb.txt b/utils/vim-lldb/doc/lldb.txt new file mode 100644 index 000000000000..e54e6f2db0dc --- /dev/null +++ b/utils/vim-lldb/doc/lldb.txt @@ -0,0 +1,115 @@ +*lldb.txt* A plugin that enables debugging from your favourite editor + +Author: Daniel Malea <daniel.malea@intel.com> +License: Same terms as Vim itself (see |license|) + +INTRODUCTION *lldb* + +Installing this plugin enables a set of commands in Vim to control the +LLDB (http://lldb.llvm.org) debugger. + +COMMANDS *lldb-commands* + +The LLDB command interpreter is exposed to Vim's command mode using the +':L' prefix. Tab-completion is available and will cycle through commands. +Some commands have modified behaviour in Vim; for example, :Lbreakpoint +with no arguments will set a breakpoint at the current cursor, rather than +printing the standard help information for the LLDB command 'breakpoint'. + + *lldb-windows* + +In addition to the standard commands available under the LLDB interpreter, +there are also commands to display or hide informational debugger panes. + +Windows can be shown or hidden using the ':Lhide <name>' or ':Lshow <name>' +commands. + *lldb-:Lhide* +:Lhide [windowname] Hide informational debugger pane named 'windowname'. + + *lldb-:Lshow* +:Lshow [windowname] Show informational debugger pane named 'windowname'. + +Possible window name arguments to the Lhide and Lshow commands include: + + * backtrace + * breakpoints + * disassembly + * locals + * registers + * threads + *lldb-:Lattach* +:Lattach <process-name> Attach to a process by name. + + *lldb-:Ldetach* +:Ldetach Detach from the current process. + + *lldb-:Ltarget* +:Ltarget [[create] executable] + Create a target with the specified executable. If + run with a single argument, that argument is assumed + to be a path to the executable to be debugged. + Otherwise, all arguments are passed into LLDB's command + interpreter. + + *lldb-:Lstart* +:Lstart Create a process by executing the current target + and wait for LLDB to attach. + + *lldb-:Lrun* +:Lrun Create a process by executing the current target + without waiting for LLDB to attach. + + *lldb-:Lcontinue* +:Lcontinue Continue execution of the process until the next + breakpoint is hit or the process exits. + + *lldb-:Lthread* +:Lthread <args> Passes through to LLDB. See :Lhelp thread. + + *lldb-:Lstep* +:Lstep Step into the current function call. + + *lldb-:Lstepin* +:Lstepin Step into the current function call. + + *lldb-:Lstepinst* +:Lstepinst Step one instruction. + + *lldb-:Lstepinstover* +:Lstepinstover Step one instruction, but skip over jump or call + instructions. + + *lldb-:Lnext* +:Lnext Step to the next line. + + *lldb-:Lfinish* +:Lfinish Step out of the current function. + + *lldb-:Lbreakpoint* +:Lbreakpoint [args] When arguments are provided, the lldb breakpoint + command is invoked. If no arguments are provided, + a breakpoint at the location under the cursor. + + *lldb-:Lprint* + *lldb-:Lpo* + *lldb-:LpO* +:Lprint <expr> Aliases to the lldb print and po commands. Cursor +:Lpo <expr> word (cursor WORD for LpO) will be used when +:LpO <expr> expression omitted. + +MAPPINGS *lldb-mappings* + +On Mac OS X (under MacVim) , the following key mappings are available: + +<Command-B> Insert a breakpoint at the line under cursor + + +ABOUT *lldb-about* + +Grab the latest version of this plugin (and LLDB sources) with: + git clone http://llvm.org/git/lldb + +File any bugs at: + http://llvm.org/bugs/enter_bug.cgi?product=lldb + + vim:tw=78:et:ft=help:norl: diff --git a/utils/vim-lldb/plugin/lldb.vim b/utils/vim-lldb/plugin/lldb.vim new file mode 100644 index 000000000000..ac5cfe3fbb43 --- /dev/null +++ b/utils/vim-lldb/plugin/lldb.vim @@ -0,0 +1,151 @@ + +" Vim script glue code for LLDB integration + +function! s:FindPythonScriptDir() + for dir in pathogen#split(&runtimepath) + let searchstr = "python-vim-lldb" + let candidates = pathogen#glob_directories(dir . "/" . searchstr) + if len(candidates) > 0 + return candidates[0] + endif + endfor + return +endfunction() + +function! s:InitLldbPlugin() + if has('python') == 0 + call confirm('ERROR: This Vim installation does not have python support. lldb.vim will not work.') + return + endif + + " Key-Bindings + " FIXME: choose sensible keybindings for: + " - process: start, interrupt, continue, continue-to-cursor + " - step: instruction, in, over, out + " + if has('gui_macvim') + " Apple-B toggles breakpoint on cursor + map <D-B> :Lbreakpoint<CR> + endif + + " + " Setup the python interpreter path + " + let vim_lldb_pydir = s:FindPythonScriptDir() + execute 'python import sys; sys.path.append("' . vim_lldb_pydir . '")' + + " + " Register :L<Command> + " The LLDB CommandInterpreter provides tab-completion in Vim's command mode. + " FIXME: this list of commands, at least partially should be auto-generated + " + + " Window show/hide commands + command -complete=custom,s:CompleteWindow -nargs=1 Lhide python ctrl.doHide('<args>') + command -complete=custom,s:CompleteWindow -nargs=0 Lshow python ctrl.doShow('<args>') + + " Launching convenience commands (no autocompletion) + command -nargs=* Lstart python ctrl.doLaunch(True, '<args>') + command -nargs=* Lrun python ctrl.doLaunch(False, '<args>') + command -nargs=1 Lattach python ctrl.doAttach('<args>') + command -nargs=0 Ldetach python ctrl.doDetach() + + " Regexp-commands: because vim's command mode does not support '_' or '-' + " characters in command names, we omit them when creating the :L<cmd> + " equivalents. + command -complete=custom,s:CompleteCommand -nargs=* Lregexpattach python ctrl.doCommand('_regexp-attach', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lregexpbreak python ctrl.doCommand('_regexp-break', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lregexpbt python ctrl.doCommand('_regexp-bt', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lregexpdown python ctrl.doCommand('_regexp-down', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lregexptbreak python ctrl.doCommand('_regexp-tbreak', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lregexpdisplay python ctrl.doCommand('_regexp-display', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lregexpundisplay python ctrl.doCommand('_regexp-undisplay', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lregexpup python ctrl.doCommand('_regexp-up', '<args>') + + command -complete=custom,s:CompleteCommand -nargs=* Lapropos python ctrl.doCommand('apropos', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lbacktrace python ctrl.doCommand('bt', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lbreakpoint python ctrl.doBreakpoint('<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lcommand python ctrl.doCommand('command', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Ldisassemble python ctrl.doCommand('disassemble', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lexpression python ctrl.doCommand('expression', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lhelp python ctrl.doCommand('help', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Llog python ctrl.doCommand('log', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lplatform python ctrl.doCommand('platform','<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lplugin python ctrl.doCommand('plugin', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lprocess python ctrl.doProcess('<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lregister python ctrl.doCommand('register', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lscript python ctrl.doCommand('script', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lsettings python ctrl.doCommand('settings','<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lsource python ctrl.doCommand('source', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Ltype python ctrl.doCommand('type', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lversion python ctrl.doCommand('version', '<args>') + command -complete=custom,s:CompleteCommand -nargs=* Lwatchpoint python ctrl.doCommand('watchpoint', '<args>') + + " Convenience (shortcut) LLDB commands + command -complete=custom,s:CompleteCommand -nargs=* Lprint python ctrl.doCommand('print', vim.eval("s:CursorWord('<args>')")) + command -complete=custom,s:CompleteCommand -nargs=* Lpo python ctrl.doCommand('po', vim.eval("s:CursorWord('<args>')")) + command -complete=custom,s:CompleteCommand -nargs=* LpO python ctrl.doCommand('po', vim.eval("s:CursorWORD('<args>')")) + command -complete=custom,s:CompleteCommand -nargs=* Lbt python ctrl.doCommand('bt', '<args>') + + " Frame/Thread-Selection (commands that also do an Uupdate but do not + " generate events in LLDB) + command -complete=custom,s:CompleteCommand -nargs=* Lframe python ctrl.doSelect('frame', '<args>') + command -complete=custom,s:CompleteCommand -nargs=? Lup python ctrl.doCommand('up', '<args>', print_on_success=False, goto_file=True) + command -complete=custom,s:CompleteCommand -nargs=? Ldown python ctrl.doCommand('down', '<args>', print_on_success=False, goto_file=True) + command -complete=custom,s:CompleteCommand -nargs=* Lthread python ctrl.doSelect('thread', '<args>') + + command -complete=custom,s:CompleteCommand -nargs=* Ltarget python ctrl.doTarget('<args>') + + " Continue + command -complete=custom,s:CompleteCommand -nargs=* Lcontinue python ctrl.doContinue() + + " Thread-Stepping (no autocompletion) + command -nargs=0 Lstepinst python ctrl.doStep(StepType.INSTRUCTION) + command -nargs=0 Lstepinstover python ctrl.doStep(StepType.INSTRUCTION_OVER) + command -nargs=0 Lstepin python ctrl.doStep(StepType.INTO) + command -nargs=0 Lstep python ctrl.doStep(StepType.INTO) + command -nargs=0 Lnext python ctrl.doStep(StepType.OVER) + command -nargs=0 Lfinish python ctrl.doStep(StepType.OUT) + + " hack: service the LLDB event-queue when the cursor moves + " FIXME: some threaded solution would be better...but it + " would have to be designed carefully because Vim's APIs are non threadsafe; + " use of the vim module **MUST** be restricted to the main thread. + command -nargs=0 Lrefresh python ctrl.doRefresh() + autocmd CursorMoved * :Lrefresh + autocmd CursorHold * :Lrefresh + autocmd VimLeavePre * python ctrl.doExit() + + execute 'pyfile ' . vim_lldb_pydir . '/plugin.py' +endfunction() + +function! s:CompleteCommand(A, L, P) + python << EOF +a = vim.eval("a:A") +l = vim.eval("a:L") +p = vim.eval("a:P") +returnCompleteCommand(a, l, p) +EOF +endfunction() + +function! s:CompleteWindow(A, L, P) + python << EOF +a = vim.eval("a:A") +l = vim.eval("a:L") +p = vim.eval("a:P") +returnCompleteWindow(a, l, p) +EOF +endfunction() + +" Returns cword if search term is empty +function! s:CursorWord(term) + return empty(a:term) ? expand('<cword>') : a:term +endfunction() + +" Returns cleaned cWORD if search term is empty +function! s:CursorWORD(term) + " Will strip all non-alphabetic characters from both sides + return empty(a:term) ? substitute(expand('<cWORD>'), '^\A*\(.\{-}\)\A*$', '\1', '') : a:term +endfunction() + +call s:InitLldbPlugin() diff --git a/utils/vim-lldb/python-vim-lldb/import_lldb.py b/utils/vim-lldb/python-vim-lldb/import_lldb.py new file mode 100644 index 000000000000..a2145d504661 --- /dev/null +++ b/utils/vim-lldb/python-vim-lldb/import_lldb.py @@ -0,0 +1,61 @@ + +# Locate and load the lldb python module + +import os, sys + +def import_lldb(): + """ Find and import the lldb modules. This function tries to find the lldb module by: + 1. Simply by doing "import lldb" in case the system python installation is aware of lldb. If that fails, + 2. Executes the lldb executable pointed to by the LLDB environment variable (or if unset, the first lldb + on PATH") with the -P flag to determine the PYTHONPATH to set. If the lldb executable returns a valid + path, it is added to sys.path and the import is attempted again. If that fails, 3. On Mac OS X the + default Xcode 4.5 installation path. + """ + + # Try simple 'import lldb', in case of a system-wide install or a pre-configured PYTHONPATH + try: + import lldb + return True + except ImportError: + pass + + # Allow overriding default path to lldb executable with the LLDB environment variable + lldb_executable = 'lldb' + if 'LLDB' in os.environ and os.path.exists(os.environ['LLDB']): + lldb_executable = os.environ['LLDB'] + + # Try using builtin module location support ('lldb -P') + from subprocess import check_output, CalledProcessError + try: + with open(os.devnull, 'w') as fnull: + lldb_minus_p_path = check_output("%s -P" % lldb_executable, shell=True, stderr=fnull).strip() + if not os.path.exists(lldb_minus_p_path): + #lldb -P returned invalid path, probably too old + pass + else: + sys.path.append(lldb_minus_p_path) + import lldb + return True + except CalledProcessError: + # Cannot run 'lldb -P' to determine location of lldb python module + pass + except ImportError: + # Unable to import lldb module from path returned by `lldb -P` + pass + + # On Mac OS X, use the try the default path to XCode lldb module + if "darwin" in sys.platform: + xcode_python_path = "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/Current/Resources/Python/" + sys.path.append(xcode_python_path) + try: + import lldb + return True + except ImportError: + # Unable to import lldb module from default Xcode python path + pass + + return False + +if not import_lldb(): + import vim + vim.command('redraw | echo "%s"' % " Error loading lldb module; vim-lldb will be disabled. Check LLDB installation or set LLDB environment variable.") diff --git a/utils/vim-lldb/python-vim-lldb/lldb_controller.py b/utils/vim-lldb/python-vim-lldb/lldb_controller.py new file mode 100644 index 000000000000..923e771e6afe --- /dev/null +++ b/utils/vim-lldb/python-vim-lldb/lldb_controller.py @@ -0,0 +1,384 @@ + +# +# This file defines the layer that talks to lldb +# + +import os, re, sys +import lldb +import vim +from vim_ui import UI + +# ================================================= +# Convert some enum value to its string counterpart +# ================================================= + +# Shamelessly copy/pasted from lldbutil.py in the test suite +def state_type_to_str(enum): + """Returns the stateType string given an enum.""" + if enum == lldb.eStateInvalid: + return "invalid" + elif enum == lldb.eStateUnloaded: + return "unloaded" + elif enum == lldb.eStateConnected: + return "connected" + elif enum == lldb.eStateAttaching: + return "attaching" + elif enum == lldb.eStateLaunching: + return "launching" + elif enum == lldb.eStateStopped: + return "stopped" + elif enum == lldb.eStateRunning: + return "running" + elif enum == lldb.eStateStepping: + return "stepping" + elif enum == lldb.eStateCrashed: + return "crashed" + elif enum == lldb.eStateDetached: + return "detached" + elif enum == lldb.eStateExited: + return "exited" + elif enum == lldb.eStateSuspended: + return "suspended" + else: + raise Exception("Unknown StateType enum") + +class StepType: + INSTRUCTION = 1 + INSTRUCTION_OVER = 2 + INTO = 3 + OVER = 4 + OUT = 5 + +class LLDBController(object): + """ Handles Vim and LLDB events such as commands and lldb events. """ + + # Timeouts (sec) for waiting on new events. Because vim is not multi-threaded, we are restricted to + # servicing LLDB events from the main UI thread. Usually, we only process events that are already + # sitting on the queue. But in some situations (when we are expecting an event as a result of some + # user interaction) we want to wait for it. The constants below set these wait period in which the + # Vim UI is "blocked". Lower numbers will make Vim more responsive, but LLDB will be delayed and higher + # numbers will mean that LLDB events are processed faster, but the Vim UI may appear less responsive at + # times. + eventDelayStep = 2 + eventDelayLaunch = 1 + eventDelayContinue = 1 + + def __init__(self): + """ Creates the LLDB SBDebugger object and initializes the UI class. """ + self.target = None + self.process = None + self.load_dependent_modules = True + + self.dbg = lldb.SBDebugger.Create() + self.commandInterpreter = self.dbg.GetCommandInterpreter() + + self.ui = UI() + + def completeCommand(self, a, l, p): + """ Returns a list of viable completions for command a with length l and cursor at p """ + + assert l[0] == 'L' + # Remove first 'L' character that all commands start with + l = l[1:] + + # Adjust length as string has 1 less character + p = int(p) - 1 + + result = lldb.SBStringList() + num = self.commandInterpreter.HandleCompletion(l, p, 1, -1, result) + + if num == -1: + # FIXME: insert completion character... what's a completion character? + pass + elif num == -2: + # FIXME: replace line with result.GetStringAtIndex(0) + pass + + if result.GetSize() > 0: + results = filter(None, [result.GetStringAtIndex(x) for x in range(result.GetSize())]) + return results + else: + return [] + + def doStep(self, stepType): + """ Perform a step command and block the UI for eventDelayStep seconds in order to process + events on lldb's event queue. + FIXME: if the step does not complete in eventDelayStep seconds, we relinquish control to + the main thread to avoid the appearance of a "hang". If this happens, the UI will + update whenever; usually when the user moves the cursor. This is somewhat annoying. + """ + if not self.process: + sys.stderr.write("No process to step") + return + + t = self.process.GetSelectedThread() + if stepType == StepType.INSTRUCTION: + t.StepInstruction(False) + if stepType == StepType.INSTRUCTION_OVER: + t.StepInstruction(True) + elif stepType == StepType.INTO: + t.StepInto() + elif stepType == StepType.OVER: + t.StepOver() + elif stepType == StepType.OUT: + t.StepOut() + + self.processPendingEvents(self.eventDelayStep, True) + + def doSelect(self, command, args): + """ Like doCommand, but suppress output when "select" is the first argument.""" + a = args.split(' ') + return self.doCommand(command, args, "select" != a[0], True) + + def doProcess(self, args): + """ Handle 'process' command. If 'launch' is requested, use doLaunch() instead + of the command interpreter to start the inferior process. + """ + a = args.split(' ') + if len(args) == 0 or (len(a) > 0 and a[0] != 'launch'): + self.doCommand("process", args) + #self.ui.update(self.target, "", self) + else: + self.doLaunch('-s' not in args, "") + + def doAttach(self, process_name): + """ Handle process attach. """ + error = lldb.SBError() + + self.processListener = lldb.SBListener("process_event_listener") + self.target = self.dbg.CreateTarget('') + self.process = self.target.AttachToProcessWithName(self.processListener, process_name, False, error) + if not error.Success(): + sys.stderr.write("Error during attach: " + str(error)) + return + + self.ui.activate() + self.pid = self.process.GetProcessID() + + print "Attached to %s (pid=%d)" % (process_name, self.pid) + + def doDetach(self): + if self.process is not None and self.process.IsValid(): + pid = self.process.GetProcessID() + state = state_type_to_str(self.process.GetState()) + self.process.Detach() + self.processPendingEvents(self.eventDelayLaunch) + + def doLaunch(self, stop_at_entry, args): + """ Handle process launch. """ + error = lldb.SBError() + + fs = self.target.GetExecutable() + exe = os.path.join(fs.GetDirectory(), fs.GetFilename()) + if self.process is not None and self.process.IsValid(): + pid = self.process.GetProcessID() + state = state_type_to_str(self.process.GetState()) + self.process.Destroy() + + launchInfo = lldb.SBLaunchInfo(args.split(' ')) + self.process = self.target.Launch(launchInfo, error) + if not error.Success(): + sys.stderr.write("Error during launch: " + str(error)) + return + + # launch succeeded, store pid and add some event listeners + self.pid = self.process.GetProcessID() + self.processListener = lldb.SBListener("process_event_listener") + self.process.GetBroadcaster().AddListener(self.processListener, lldb.SBProcess.eBroadcastBitStateChanged) + + print "Launched %s %s (pid=%d)" % (exe, args, self.pid) + + if not stop_at_entry: + self.doContinue() + else: + self.processPendingEvents(self.eventDelayLaunch) + + def doTarget(self, args): + """ Pass target command to interpreter, except if argument is not one of the valid options, or + is create, in which case try to create a target with the argument as the executable. For example: + target list ==> handled by interpreter + target create blah ==> custom creation of target 'blah' + target blah ==> also creates target blah + """ + target_args = [#"create", + "delete", + "list", + "modules", + "select", + "stop-hook", + "symbols", + "variable"] + + a = args.split(' ') + if len(args) == 0 or (len(a) > 0 and a[0] in target_args): + self.doCommand("target", args) + return + elif len(a) > 1 and a[0] == "create": + exe = a[1] + elif len(a) == 1 and a[0] not in target_args: + exe = a[0] + + err = lldb.SBError() + self.target = self.dbg.CreateTarget(exe, None, None, self.load_dependent_modules, err) + if not self.target: + sys.stderr.write("Error creating target %s. %s" % (str(exe), str(err))) + return + + self.ui.activate() + self.ui.update(self.target, "created target %s" % str(exe), self) + + def doContinue(self): + """ Handle 'contiue' command. + FIXME: switch to doCommand("continue", ...) to handle -i ignore-count param. + """ + if not self.process or not self.process.IsValid(): + sys.stderr.write("No process to continue") + return + + self.process.Continue() + self.processPendingEvents(self.eventDelayContinue) + + def doBreakpoint(self, args): + """ Handle breakpoint command with command interpreter, except if the user calls + "breakpoint" with no other args, in which case add a breakpoint at the line + under the cursor. + """ + a = args.split(' ') + if len(args) == 0: + show_output = False + + # User called us with no args, so toggle the bp under cursor + cw = vim.current.window + cb = vim.current.buffer + name = cb.name + line = cw.cursor[0] + + # Since the UI is responsbile for placing signs at bp locations, we have to + # ask it if there already is one or more breakpoints at (file, line)... + if self.ui.haveBreakpoint(name, line): + bps = self.ui.getBreakpoints(name, line) + args = "delete %s" % " ".join([str(b.GetID()) for b in bps]) + self.ui.deleteBreakpoints(name, line) + else: + args = "set -f %s -l %d" % (name, line) + else: + show_output = True + + self.doCommand("breakpoint", args, show_output) + return + + def doRefresh(self): + """ process pending events and update UI on request """ + status = self.processPendingEvents() + + def doShow(self, name): + """ handle :Lshow <name> """ + if not name: + self.ui.activate() + return + + if self.ui.showWindow(name): + self.ui.update(self.target, "", self) + + def doHide(self, name): + """ handle :Lhide <name> """ + if self.ui.hideWindow(name): + self.ui.update(self.target, "", self) + + def doExit(self): + self.dbg.Terminate() + self.dbg = None + + def getCommandResult(self, command, command_args): + """ Run cmd in the command interpreter and returns (success, output) """ + result = lldb.SBCommandReturnObject() + cmd = "%s %s" % (command, command_args) + + self.commandInterpreter.HandleCommand(cmd, result) + return (result.Succeeded(), result.GetOutput() if result.Succeeded() else result.GetError()) + + def doCommand(self, command, command_args, print_on_success = True, goto_file=False): + """ Run cmd in interpreter and print result (success or failure) on the vim status line. """ + (success, output) = self.getCommandResult(command, command_args) + if success: + self.ui.update(self.target, "", self, goto_file) + if len(output) > 0 and print_on_success: + print output + else: + sys.stderr.write(output) + + def getCommandOutput(self, command, command_args=""): + """ runs cmd in the command interpreter andreturns (status, result) """ + result = lldb.SBCommandReturnObject() + cmd = "%s %s" % (command, command_args) + self.commandInterpreter.HandleCommand(cmd, result) + return (result.Succeeded(), result.GetOutput() if result.Succeeded() else result.GetError()) + + def processPendingEvents(self, wait_seconds=0, goto_file=True): + """ Handle any events that are queued from the inferior. + Blocks for at most wait_seconds, or if wait_seconds == 0, + process only events that are already queued. + """ + + status = None + num_events_handled = 0 + + if self.process is not None: + event = lldb.SBEvent() + old_state = self.process.GetState() + new_state = None + done = False + if old_state == lldb.eStateInvalid or old_state == lldb.eStateExited: + # Early-exit if we are in 'boring' states + pass + else: + while not done and self.processListener is not None: + if not self.processListener.PeekAtNextEvent(event): + if wait_seconds > 0: + # No events on the queue, but we are allowed to wait for wait_seconds + # for any events to show up. + self.processListener.WaitForEvent(wait_seconds, event) + new_state = lldb.SBProcess.GetStateFromEvent(event) + + num_events_handled += 1 + + done = not self.processListener.PeekAtNextEvent(event) + else: + # An event is on the queue, process it here. + self.processListener.GetNextEvent(event) + new_state = lldb.SBProcess.GetStateFromEvent(event) + + # continue if stopped after attaching + if old_state == lldb.eStateAttaching and new_state == lldb.eStateStopped: + self.process.Continue() + + # If needed, perform any event-specific behaviour here + num_events_handled += 1 + + if num_events_handled == 0: + pass + else: + if old_state == new_state: + status = "" + self.ui.update(self.target, status, self, goto_file) + + +def returnCompleteCommand(a, l, p): + """ Returns a "\n"-separated string with possible completion results + for command a with length l and cursor at p. + """ + separator = "\n" + results = ctrl.completeCommand(a, l, p) + vim.command('return "%s%s"' % (separator.join(results), separator)) + +def returnCompleteWindow(a, l, p): + """ Returns a "\n"-separated string with possible completion results + for commands that expect a window name parameter (like hide/show). + FIXME: connect to ctrl.ui instead of hardcoding the list here + """ + separator = "\n" + results = ['breakpoints', 'backtrace', 'disassembly', 'locals', 'threads', 'registers'] + vim.command('return "%s%s"' % (separator.join(results), separator)) + +global ctrl +ctrl = LLDBController() diff --git a/utils/vim-lldb/python-vim-lldb/plugin.py b/utils/vim-lldb/python-vim-lldb/plugin.py new file mode 100644 index 000000000000..694783a95b0e --- /dev/null +++ b/utils/vim-lldb/python-vim-lldb/plugin.py @@ -0,0 +1,14 @@ + +# Try to import all dependencies, catch and handle the error gracefully if it fails. + +import import_lldb + +try: + import lldb + import vim +except ImportError: + sys.stderr.write("Unable to load vim/lldb module. Check lldb is on the path is available (or LLDB is set) and that script is invoked inside Vim with :pyfile") + pass +else: + # Everthing went well, so use import to start the plugin controller + from lldb_controller import * diff --git a/utils/vim-lldb/python-vim-lldb/vim_panes.py b/utils/vim-lldb/python-vim-lldb/vim_panes.py new file mode 100644 index 000000000000..ec537199922c --- /dev/null +++ b/utils/vim-lldb/python-vim-lldb/vim_panes.py @@ -0,0 +1,618 @@ +# +# 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 == 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 == 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") diff --git a/utils/vim-lldb/python-vim-lldb/vim_signs.py b/utils/vim-lldb/python-vim-lldb/vim_signs.py new file mode 100644 index 000000000000..926cc29a7fca --- /dev/null +++ b/utils/vim-lldb/python-vim-lldb/vim_signs.py @@ -0,0 +1,73 @@ + +# Classes responsible for drawing signs in the Vim user interface. + +import vim + +class VimSign(object): + SIGN_TEXT_BREAKPOINT_RESOLVED = "B>" + SIGN_TEXT_BREAKPOINT_UNRESOLVED = "b>" + SIGN_TEXT_PC = "->" + SIGN_HIGHLIGHT_COLOUR_PC = 'darkblue' + + # unique sign id (for ':[sign/highlight] define) + sign_id = 1 + + # unique name id (for ':sign place') + name_id = 1 + + # Map of {(sign_text, highlight_colour) --> sign_name} + defined_signs = {} + + def __init__(self, sign_text, buffer, line_number, highlight_colour=None): + """ Define the sign and highlight (if applicable) and show the sign. """ + + # Get the sign name, either by defining it, or looking it up in the map of defined signs + key = (sign_text, highlight_colour) + if not key in VimSign.defined_signs: + name = self.define(sign_text, highlight_colour) + else: + name = VimSign.defined_signs[key] + + self.show(name, buffer.number, line_number) + pass + + def define(self, sign_text, highlight_colour): + """ Defines sign and highlight (if highlight_colour is not None). """ + sign_name = "sign%d" % VimSign.name_id + if highlight_colour is None: + vim.command("sign define %s text=%s" % (sign_name, sign_text)) + else: + self.highlight_name = "highlight%d" % VimSign.name_id + vim.command("highlight %s ctermbg=%s guibg=%s" % (self.highlight_name, + highlight_colour, + highlight_colour)) + vim.command("sign define %s text=%s linehl=%s texthl=%s" % (sign_name, + sign_text, + self.highlight_name, + self.highlight_name)) + VimSign.defined_signs[(sign_text, highlight_colour)] = sign_name + VimSign.name_id += 1 + return sign_name + + + def show(self, name, buffer_number, line_number): + self.id = VimSign.sign_id + VimSign.sign_id += 1 + vim.command("sign place %d name=%s line=%d buffer=%s" % (self.id, name, line_number, buffer_number)) + pass + + def hide(self): + vim.command("sign unplace %d" % self.id) + pass + +class BreakpointSign(VimSign): + def __init__(self, buffer, line_number, is_resolved): + txt = VimSign.SIGN_TEXT_BREAKPOINT_RESOLVED if is_resolved else VimSign.SIGN_TEXT_BREAKPOINT_UNRESOLVED + super(BreakpointSign, self).__init__(txt, buffer, line_number) + +class PCSign(VimSign): + def __init__(self, buffer, line_number, is_selected_thread): + super(PCSign, self).__init__(VimSign.SIGN_TEXT_PC, + buffer, + line_number, + VimSign.SIGN_HIGHLIGHT_COLOUR_PC if is_selected_thread else None) diff --git a/utils/vim-lldb/python-vim-lldb/vim_ui.py b/utils/vim-lldb/python-vim-lldb/vim_ui.py new file mode 100644 index 000000000000..4be346b96f0e --- /dev/null +++ b/utils/vim-lldb/python-vim-lldb/vim_ui.py @@ -0,0 +1,235 @@ + +# LLDB UI state in the Vim user interface. + +import os, re, sys +import lldb +import vim +from vim_panes import * +from vim_signs import * + +def is_same_file(a, b): + """ returns true if paths a and b are the same file """ + a = os.path.realpath(a) + b = os.path.realpath(b) + return a in b or b in a + +class UI: + def __init__(self): + """ Declare UI state variables """ + + # Default panes to display + self.defaultPanes = ['breakpoints', 'backtrace', 'locals', 'threads', 'registers', 'disassembly'] + + # map of tuples (filename, line) --> SBBreakpoint + self.markedBreakpoints = {} + + # Currently shown signs + self.breakpointSigns = {} + self.pcSigns = [] + + # Container for panes + self.paneCol = PaneLayout() + + # All possible LLDB panes + self.backtracePane = BacktracePane(self.paneCol) + self.threadPane = ThreadPane(self.paneCol) + self.disassemblyPane = DisassemblyPane(self.paneCol) + self.localsPane = LocalsPane(self.paneCol) + self.registersPane = RegistersPane(self.paneCol) + self.breakPane = BreakpointsPane(self.paneCol) + + def activate(self): + """ Activate UI: display default set of panes """ + self.paneCol.prepare(self.defaultPanes) + + def get_user_buffers(self, filter_name=None): + """ Returns a list of buffers that are not a part of the LLDB UI. That is, they + are not contained in the PaneLayout object self.paneCol. + """ + ret = [] + for w in vim.windows: + b = w.buffer + if not self.paneCol.contains(b.name): + if filter_name is None or filter_name in b.name: + ret.append(b) + return ret + + def update_pc(self, process, buffers, goto_file): + """ Place the PC sign on the PC location of each thread's selected frame """ + + def GetPCSourceLocation(thread): + """ Returns a tuple (thread_index, file, line, column) that represents where + the PC sign should be placed for a thread. + """ + + frame = thread.GetSelectedFrame() + frame_num = frame.GetFrameID() + le = frame.GetLineEntry() + while not le.IsValid() and frame_num < thread.GetNumFrames(): + frame_num += 1 + le = thread.GetFrameAtIndex(frame_num).GetLineEntry() + + if le.IsValid(): + path = os.path.join(le.GetFileSpec().GetDirectory(), le.GetFileSpec().GetFilename()) + return (thread.GetIndexID(), path, le.GetLine(), le.GetColumn()) + return None + + + # Clear all existing PC signs + del_list = [] + for sign in self.pcSigns: + sign.hide() + del_list.append(sign) + for sign in del_list: + self.pcSigns.remove(sign) + del sign + + # Select a user (non-lldb) window + if not self.paneCol.selectWindow(False): + # No user window found; avoid clobbering by splitting + vim.command(":vsp") + + # Show a PC marker for each thread + for thread in process: + loc = GetPCSourceLocation(thread) + if not loc: + # no valid source locations for PCs. hide all existing PC markers + continue + + buf = None + (tid, fname, line, col) = loc + buffers = self.get_user_buffers(fname) + is_selected = thread.GetIndexID() == process.GetSelectedThread().GetIndexID() + if len(buffers) == 1: + buf = buffers[0] + if buf != vim.current.buffer: + # Vim has an open buffer to the required file: select it + vim.command('execute ":%db"' % buf.number) + elif is_selected and vim.current.buffer.name not in fname and os.path.exists(fname) and goto_file: + # FIXME: If current buffer is modified, vim will complain when we try to switch away. + # Find a way to detect if the current buffer is modified, and...warn instead? + vim.command('execute ":e %s"' % fname) + buf = vim.current.buffer + elif len(buffers) > 1 and goto_file: + #FIXME: multiple open buffers match PC location + continue + else: + continue + + self.pcSigns.append(PCSign(buf, line, is_selected)) + + if is_selected and goto_file: + # if the selected file has a PC marker, move the cursor there too + curname = vim.current.buffer.name + if curname is not None and is_same_file(curname, fname): + move_cursor(line, 0) + elif move_cursor: + print "FIXME: not sure where to move cursor because %s != %s " % (vim.current.buffer.name, fname) + + def update_breakpoints(self, target, buffers): + """ Decorates buffer with signs corresponding to breakpoints in target. """ + + def GetBreakpointLocations(bp): + """ Returns a list of tuples (resolved, filename, line) where a breakpoint was resolved. """ + if not bp.IsValid(): + sys.stderr.write("breakpoint is invalid, no locations") + return [] + + ret = [] + numLocs = bp.GetNumLocations() + for i in range(numLocs): + loc = bp.GetLocationAtIndex(i) + desc = get_description(loc, lldb.eDescriptionLevelFull) + match = re.search('at\ ([^:]+):([\d]+)', desc) + try: + lineNum = int(match.group(2).strip()) + ret.append((loc.IsResolved(), match.group(1), lineNum)) + except ValueError as e: + sys.stderr.write("unable to parse breakpoint location line number: '%s'" % match.group(2)) + sys.stderr.write(str(e)) + + return ret + + + if target is None or not target.IsValid(): + return + + needed_bps = {} + for bp_index in range(target.GetNumBreakpoints()): + bp = target.GetBreakpointAtIndex(bp_index) + locations = GetBreakpointLocations(bp) + for (is_resolved, file, line) in GetBreakpointLocations(bp): + for buf in buffers: + if file in buf.name: + needed_bps[(buf, line, is_resolved)] = bp + + # Hide any signs that correspond with disabled breakpoints + del_list = [] + for (b, l, r) in self.breakpointSigns: + if (b, l, r) not in needed_bps: + self.breakpointSigns[(b, l, r)].hide() + del_list.append((b, l, r)) + for d in del_list: + del self.breakpointSigns[d] + + # Show any signs for new breakpoints + for (b, l, r) in needed_bps: + bp = needed_bps[(b, l, r)] + if self.haveBreakpoint(b.name, l): + self.markedBreakpoints[(b.name, l)].append(bp) + else: + self.markedBreakpoints[(b.name, l)] = [bp] + + if (b, l, r) not in self.breakpointSigns: + s = BreakpointSign(b, l, r) + self.breakpointSigns[(b, l, r)] = s + + def update(self, target, status, controller, goto_file=False): + """ Updates debugger info panels and breakpoint/pc marks and prints + status to the vim status line. If goto_file is True, the user's + cursor is moved to the source PC location in the selected frame. + """ + + self.paneCol.update(target, controller) + self.update_breakpoints(target, self.get_user_buffers()) + + if target is not None and target.IsValid(): + process = target.GetProcess() + if process is not None and process.IsValid(): + self.update_pc(process, self.get_user_buffers, goto_file) + + if status is not None and len(status) > 0: + print status + + def haveBreakpoint(self, file, line): + """ Returns True if we have a breakpoint at file:line, False otherwise """ + return (file, line) in self.markedBreakpoints + + def getBreakpoints(self, fname, line): + """ Returns the LLDB SBBreakpoint object at fname:line """ + if self.haveBreakpoint(fname, line): + return self.markedBreakpoints[(fname, line)] + else: + return None + + def deleteBreakpoints(self, name, line): + del self.markedBreakpoints[(name, line)] + + def showWindow(self, name): + """ Shows (un-hides) window pane specified by name """ + if not self.paneCol.havePane(name): + sys.stderr.write("unknown window: %s" % name) + return False + self.paneCol.prepare([name]) + return True + + def hideWindow(self, name): + """ Hides window pane specified by name """ + if not self.paneCol.havePane(name): + sys.stderr.write("unknown window: %s" % name) + return False + self.paneCol.hide([name]) + return True + +global ui +ui = UI() |