├── .gitignore ├── README.md ├── doc └── lldb.txt ├── plugin └── lldb.vim ├── python-vim-lldb ├── import_lldb.py ├── lldb_controller.py ├── plugin.py ├── vim_panes.py ├── vim_signs.py └── vim_ui.py └── vim-lldb.png /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LLDB Vim Frontend (UNMAINTAINED) 2 | ================= 3 | 4 | This plugin provides lldb debbuging integration including 5 | 6 | * breakpoints 7 | * watchpoints 8 | * threads view 9 | * locals view 10 | * and more ... 11 | 12 | Arbitrary valid lldb commands can be invoked as well. 13 | 14 | **NOTE** : This is a fork of the plugin which is part of the llvm distribution. The original 15 | can be found at http://llvm.org/svn/llvm-project/lldb/trunk/utils/vim-lldb/. 16 | 17 | ![](https://raw.github.com/gilligan/vim-lldb/master/vim-lldb.png) 18 | 19 | Prerequisites 20 | ------------- 21 | 22 | * vim >= 7.3 on Linux or OSX with python support built in 23 | * lldb executable needs to be in the path 24 | 25 | Installation 26 | ------------ 27 | 28 | Installation is easiest using a package manager such as bundle: 29 | 30 | Bundle "gilligan/vim-lldb" 31 | 32 | Of course you are free to manually copy the files to your vimrc folder if you prefer that for 33 | whatever weird reason. 34 | 35 | 36 | Usage/Getting Help 37 | ------------------ 38 | 39 | Please refer to the vim help for a short 'getting started' section and 40 | information on the available commands and configuration options. (:he lldb). 41 | -------------------------------------------------------------------------------- /doc/lldb.txt: -------------------------------------------------------------------------------- 1 | *lldb.txt* A plugin that enables debugging from your favourite editor 2 | 3 | _ ____ ____ 4 | _ __(_)___ ___ / / /___/ / /_ 5 | | | / / / __ `__ \______/ / / __ / __ \ 6 | | |/ / / / / / / /_____/ / / /_/ / /_/ / 7 | |___/_/_/ /_/ /_/ /_/_/\__,_/_.___/ 8 | 9 | LLDB Debugger Ingegration For VIM 10 | 11 | ============================================================================== 12 | 13 | CONTENTS *lldb-contents* 14 | 15 | 1. Introduction .................... |lldb-intro| 16 | 2. Getting Started ................. |lldb-start| 17 | 3. Commands ........................ |lldb-commands| 18 | 4. Mappings ........................ |lldb-mappings| 19 | 4. License ......................... |lldb-license| 20 | 5. Bugs ............................ |lldb-bugs| 21 | 6. Contributing .................... |lldb-contributing| 22 | 23 | 24 | Maintainer : Tobias Pflug 25 | Original Author: Daniel Malea 26 | License: Same terms as Vim itself (see |license|) 27 | 28 | INTRODUCTION *lldb-intro* 29 | 30 | The plugin provides an interface to the lldb debugger allowing for 31 | convenient debugging sessions inside your favorite editor including 32 | features such as breakpoints, stepping, watchpoints etc. 33 | 34 | The original plugin can be found here: 35 | 36 | http://llvm.org/svn/llvm-project/lldb/trunk/utils/vim-lldb/ 37 | 38 | Credit for pretty much all current functionality goes to the original 39 | authors. Currently only minor modifications have been made to the 40 | original plugin. 41 | 42 | 43 | GETTING STARTED *lldb-start* 44 | 45 | To quickly get started compile (don't forget to compile with debugging 46 | symbols) and start some program. Then open a source file belonging to the 47 | program in vim and execute ':Lattach .' Then select some 48 | line in the source file and execute ':Lbreakpoint' to set a breakpint at 49 | the current line. 50 | 51 | Once the program reaches the specified breakpoint you will be able to 52 | inspect state and step through the proram using the commands described below. 53 | 54 | COMMANDS *lldb-commands* 55 | 56 | The LLDB command interpreter is exposed to Vim's command mode using the 57 | ':L' prefix. Tab-completion is available and will cycle through commands. 58 | Some commands have modified behaviour in Vim; for example, :Lbreakpoint 59 | with no arguments will set a breakpoint at the current cursor, rather than 60 | printing the standard help information for the LLDB command 'breakpoint'. 61 | 62 | *lldb-windows* 63 | 64 | In addition to the standard commands available under the LLDB interpreter, 65 | there are also commands to display or hide informational debugger panes. 66 | 67 | Windows can be shown or hidden using the ':Lhide ' or ':Lshow ' 68 | commands. 69 | *lldb-:Lhide* 70 | :Lhide [windowname] Hide informational debugger pane named 'windowname'. 71 | 72 | *lldb-:Lshow* 73 | :Lshow [windowname] Show informational debugger pane named 'windowname'. 74 | 75 | Possible window name arguments to the Lhide and Lshow commands include: 76 | 77 | * backtrace 78 | * breakpoints 79 | * disassembly 80 | * locals 81 | * registers 82 | * threads 83 | *lldb-:Lattach* 84 | :Lattach Attach to a process by name. 85 | 86 | *lldb-:Ldetach* 87 | :Ldetach Detach from the current process. 88 | 89 | *lldb-:Ltarget* 90 | :Ltarget [[create] executable] 91 | Create a target with the specified executable. If 92 | run with a single argument, that argument is assumed 93 | to be a path to the executable to be debugged. 94 | Otherwise, all arguments are passed into LLDB's command 95 | interpreter. 96 | 97 | *lldb-:Lstart* 98 | :Lstart Create a process by executing the current target 99 | and wait for LLDB to attach. 100 | 101 | *lldb-:Lrun* 102 | :Lrun Create a process by executing the current target 103 | without waiting for LLDB to attach. 104 | 105 | *lldb-:Lcontinue* 106 | :Lcontinue Continue execution of the process until the next 107 | breakpoint is hit or the process exits. 108 | 109 | *lldb-:Lthread* 110 | :Lthread Passes through to LLDB. See :Lhelp thread. 111 | 112 | *lldb-:Lstep* 113 | :Lstep Step into the current function call. 114 | 115 | *lldb-:Lstepin* 116 | :Lstepin Step into the current function call. 117 | 118 | *lldb-:Lstepinst* 119 | :Lstepinst Step one instruction. 120 | 121 | *lldb-:Lstepinstover* 122 | :Lstepinstover Step one instruction, but skip over jump or call 123 | instructions. 124 | 125 | *lldb-:Lnext* 126 | :Lnext Step to the next line. 127 | 128 | *lldb-:Lfinish* 129 | :Lfinish Step out of the current function. 130 | 131 | *lldb-:Lbreakpoint* 132 | :Lbreakpoint [args] When arguments are provided, the lldb breakpoint 133 | command is invoked. If no arguments are provided, 134 | a breakpoint at the location under the cursor. 135 | 136 | *lldb-:Lprint* 137 | *lldb-:Lpo* 138 | *lldb-:LpO* 139 | :Lprint Aliases to the lldb print and po commands. Cursor 140 | :Lpo word (cursor WORD for LpO) will be used when 141 | :LpO expression omitted. 142 | 143 | MAPPINGS *lldb-mappings* 144 | 145 | There are no default mappings defined by the plugin. All commands described 146 | above can be mapped by defining a respective variable: 147 | 148 | let g:lldb_map_Lframe = "f" 149 | 150 | This will map the Lframe command to "f". Other commands can be 151 | mapped accordingly using 'lldb_map_' + . 152 | 153 | 154 | LICENSE *lldb-license* 155 | 156 | Same as Vim itself. 157 | 158 | BUGS *lldb-bugs* 159 | 160 | If you run into a bug use the github issue tracker to report it: 161 | http://github.com/gilligan/vim-lldb/issues/ 162 | 163 | CONTRIBUTING *lldb-contributing* 164 | 165 | If you want to help out you are more then welcome to do so. In fact 166 | I am sure there are plenty of people out there that will do a better 167 | job with this plugin than me. My C skills are more than rusty. I mostly 168 | wanted to make this nice plugin more public and host it in a way in which 169 | it is easy to install with your favorite plugin manager. 170 | 171 | Long story short: Bring on your forks and pull requests. 172 | 173 | vim:tw=78:sw=4:ft=help:norl: 174 | -------------------------------------------------------------------------------- /plugin/lldb.vim: -------------------------------------------------------------------------------- 1 | " --------------------------------------------------------------------- 2 | " File: lldb.vim 3 | " Description: LLDB Debugger Integration Plugin 4 | " Maintainer: Tobias Pflug 5 | " License: Same License as Vim itself 6 | " -------------------------------------------------------------------- 7 | 8 | if (exists('g:loaded_lldb') && g:loaded_lldb) || v:version < 703 || &cp || !has('python') 9 | finish 10 | endif 11 | let g:loaded_lldb = 1 12 | 13 | " 14 | " format of the command entries is as follows: 15 | " [ command name , completion function, nargs, python code, keymap] 16 | " 17 | let s:lldb_commands = [ 18 | \ ['Lhide', 's:CompleteWindow', '1', 'ctrl.doHide("")'], 19 | \ ['Lshow', 's:CompleteWindow', '1', 'ctrl.doShow("")'], 20 | \ ['Lstart', '', '*', 'ctrl.doLaunch(True, "")'], 21 | \ ['Lrun', '', '*', 'ctrl.doLaunch(False, "")'], 22 | \ ['Lattach', '', '1', 'ctrl.doAttach("")'], 23 | \ ['Ldetach', '', '0', 'ctrl.doDetach()'], 24 | \ ['Lregexpattach', 's:CompleteCommand', '*', 'ctrl.doCommand("_regexp-attach", "")'], 25 | \ ['Lregexpbreak', 's:CompleteCommand', '*', 'ctrl.doCommand("_regexp-break", "")'], 26 | \ ['Lregexpbt', 's:CompleteCommand', '*', 'ctrl.doCommand("_regexp-bt", "")'], 27 | \ ['Lregexpdown', 's:CompleteCommand', '*', 'ctrl.doCommand("_regexp-down", "")'], 28 | \ ['Lregexptbreak', 's:CompleteCommand', '*', 'ctrl.doCommand("_regexp-tbreak", "")'], 29 | \ ['Lregexpdisplay', 's:CompleteCommand', '*', 'ctrl.doCommand("_regexp-display", "")'], 30 | \ ['Lregexpundisplay', 's:CompleteCommand', '*', 'ctrl.doCommand("_regexp-undisplay", "")'], 31 | \ ['Lregexpup', 's:CompleteCommand', '*', 'ctrl.doCommand("_regexp-up", "")'], 32 | \ ['Lapropos', 's:CompleteCommand', '*', 'ctrl.doCommand("apropos", "")'], 33 | \ ['Lbacktrace', 's:CompleteCommand', '*', 'ctrl.doCommand("bt", "")'], 34 | \ ['Lbreakpoint', 's:CompleteCommand', '*', 'ctrl.doBreakpoint("")', 'lb'], 35 | \ ["Lcommand", "s:CompleteCommand", "*", 'ctrl.doCommand("command", "")'], 36 | \ ["Ldisassemble", "s:CompleteCommand", "*", 'ctrl.doCommand("disassemble", "")'], 37 | \ ["Lexpression", "s:CompleteCommand", "*", 'ctrl.doCommand("expression", "")'], 38 | \ ["Lhelp", "s:CompleteCommand", "*", 'ctrl.doCommand("help", "")'], 39 | \ ["Llog", "s:CompleteCommand", "*", 'ctrl.doCommand("log", "")'], 40 | \ ["Lplatform", "s:CompleteCommand", "*", 'ctrl.doCommand("platform","")'], 41 | \ ["Lplugin", "s:CompleteCommand", "*", 'ctrl.doCommand("plugin", "")'], 42 | \ ["Lprocess", "s:CompleteCommand", "*", 'ctrl.doProcess("")'], 43 | \ ["Lregister", "s:CompleteCommand", "*", 'ctrl.doCommand("register", "")'], 44 | \ ["Lscript", "s:CompleteCommand", "*", 'ctrl.doCommand("script", "")'], 45 | \ ["Lsettings", "s:CompleteCommand", "*", 'ctrl.doCommand("settings","")'], 46 | \ ["Lsource", "s:CompleteCommand", "*", 'ctrl.doCommand("source", "")'], 47 | \ ["Ltype", "s:CompleteCommand", "*", 'ctrl.doCommand("type", "")'], 48 | \ ["Lversion", "s:CompleteCommand", "*", 'ctrl.doCommand("version", "")'], 49 | \ ["Lwatchpoint", "s:CompleteCommand", "*", 'ctrl.doCommand("watchpoint", "")'], 50 | \ ["Lprint", "s:CompleteCommand", "*", 'ctrl.doCommand("print", vim.eval("s:CursorWord("")"))'], 51 | \ ["Lpo", "s:CompleteCommand", "*", 'ctrl.doCommand("po", vim.eval("s:CursorWord("")"))'], 52 | \ ["LpO", "s:CompleteCommand", "*", 'ctrl.doCommand("po", vim.eval("s:CursorWORD("")"))'], 53 | \ ["Lbt", "s:CompleteCommand", "*", 'ctrl.doCommand("bt", "")'], 54 | \ ["Lframe", "s:CompleteCommand", "*", 'ctrl.doSelect("frame", "")'], 55 | \ ["Lup", "s:CompleteCommand", "?", 'ctrl.doCommand("up", "", print_on_success=False, goto_file=True)'], 56 | \ ["Ldown", "s:CompleteCommand", "?", 'ctrl.doCommand("down", "", print_on_success=False, goto_file=True)'], 57 | \ ["Lthread", "s:CompleteCommand", "*", 'ctrl.doSelect("thread", "")'], 58 | \ ["Ltarget", "s:CompleteCommand", "*", 'ctrl.doTarget("")'], 59 | \ ['Lcontinue', "s:CompleteCommand", "*", 'ctrl.doContinue()', 'lc'], 60 | \ ['Lstepinst', "", "0", 'ctrl.doStep(StepType.INSTRUCTION)'], 61 | \ ['Lstepinstover', "", "0", 'ctrl.doStep(StepType.INSTRUCTION_OVER)'], 62 | \ ['Lstepin', "", "0", 'ctrl.doStep(StepType.INTO)'], 63 | \ ['Lstep', "", "0", 'ctrl.doStep(StepType.INTO)', 'li'], 64 | \ ['Lnext', "", "0", 'ctrl.doStep(StepType.OVER)', 'ln'], 65 | \ ['Lfinish', "", "0", 'ctrl.doStep(StepType.OUT)'], 66 | \ ['Lrefresh', "", "0", 'ctrl.doRefresh()', 'lr'] 67 | \] 68 | 69 | " Python module init {{{ 70 | function! lldb#pythonInit() 71 | execute 'python import sys' 72 | let python_module_dir = fnameescape(globpath(&runtimepath, 'python-vim-lldb')) 73 | execute 'python sys.path.append("' . python_module_dir . '")' 74 | execute 'pyfile ' . python_module_dir . '/plugin.py' 75 | endfunction 76 | " }}} 77 | 78 | 79 | " Command registration {{{ 80 | function! lldb#createCommands() 81 | for cmd in s:lldb_commands 82 | let complFun = '' 83 | let nargs = '' 84 | if len(cmd[1]) > 0 85 | let complFun = '-complete=custom,' . cmd[1] 86 | endif 87 | if len(cmd[2]) > 0 88 | let nargs = '-nargs=' . cmd[2] 89 | endif 90 | execute 'command ' . complFun . ' ' . nargs . ' ' . cmd[0] . ' python ' . cmd[3] 91 | endfor 92 | " hack: service the LLDB event-queue when the cursor moves 93 | autocmd CursorMoved * :Lrefresh 94 | autocmd CursorHold * :Lrefresh 95 | autocmd VimLeavePre * python ctrl.doExit() 96 | endfunction 97 | " 98 | 99 | function lldb#createKeyMaps() 100 | for cmd in s:lldb_commands 101 | " only map what has been configured by the user 102 | if exists('g:lldb_map_' . cmd[0]) 103 | execute 'nnoremap ' . eval('g:lldb_map_' . cmd[0]) . ' :' . cmd[0] . '' 104 | endif 105 | endfor 106 | endfunction 107 | 108 | function! s:InitLldbPlugin() 109 | call lldb#pythonInit() 110 | call lldb#createCommands() 111 | call lldb#createKeyMaps() 112 | endfunction() 113 | " }}} 114 | 115 | 116 | " Command Completion Functions {{{ 117 | function! s:CompleteCommand(A, L, P) 118 | python << EOF 119 | a = vim.eval("a:A") 120 | l = vim.eval("a:L") 121 | p = vim.eval("a:P") 122 | returnCompleteCommand(a, l, p) 123 | EOF 124 | endfunction() 125 | 126 | function! s:CompleteWindow(A, L, P) 127 | python << EOF 128 | a = vim.eval("a:A") 129 | l = vim.eval("a:L") 130 | p = vim.eval("a:P") 131 | returnCompleteWindow(a, l, p) 132 | EOF 133 | endfunction() 134 | 135 | " Returns cword if search term is empty 136 | function! s:CursorWord(term) 137 | return empty(a:term) ? expand('') : a:term 138 | endfunction() 139 | 140 | " Returns cleaned cWORD if search term is empty 141 | function! s:CursorWORD(term) 142 | " Will strip all non-alphabetic characters from both sides 143 | return empty(a:term) ? substitute(expand(''), '^\A*\(.\{-}\)\A*$', '\1', '') : a:term 144 | endfunction() 145 | " }}} 146 | 147 | 148 | call s:InitLldbPlugin() 149 | 150 | -------------------------------------------------------------------------------- /python-vim-lldb/import_lldb.py: -------------------------------------------------------------------------------- 1 | 2 | # Locate and load the lldb python module 3 | 4 | import os, sys 5 | 6 | def import_lldb(): 7 | """ Find and import the lldb modules. This function tries to find the lldb module by: 8 | 1. Simply by doing "import lldb" in case the system python installation is aware of lldb. If that fails, 9 | 2. Executes the lldb executable pointed to by the LLDB environment variable (or if unset, the first lldb 10 | on PATH") with the -P flag to determine the PYTHONPATH to set. If the lldb executable returns a valid 11 | path, it is added to sys.path and the import is attempted again. If that fails, 3. On Mac OS X the 12 | default Xcode 4.5 installation path. 13 | """ 14 | 15 | # Try simple 'import lldb', in case of a system-wide install or a pre-configured PYTHONPATH 16 | try: 17 | import lldb 18 | return True 19 | except ImportError: 20 | pass 21 | 22 | # Allow overriding default path to lldb executable with the LLDB environment variable 23 | lldb_executable = 'lldb' 24 | if 'LLDB' in os.environ and os.path.exists(os.environ['LLDB']): 25 | lldb_executable = os.environ['LLDB'] 26 | 27 | # Try using builtin module location support ('lldb -P') 28 | from subprocess import check_output, CalledProcessError 29 | try: 30 | with open(os.devnull, 'w') as fnull: 31 | lldb_minus_p_path = check_output("%s -P" % lldb_executable, shell=True, stderr=fnull).strip() 32 | if not os.path.exists(lldb_minus_p_path): 33 | #lldb -P returned invalid path, probably too old 34 | pass 35 | else: 36 | sys.path.append(lldb_minus_p_path) 37 | import lldb 38 | return True 39 | except CalledProcessError: 40 | # Cannot run 'lldb -P' to determine location of lldb python module 41 | pass 42 | except ImportError: 43 | # Unable to import lldb module from path returned by `lldb -P` 44 | pass 45 | 46 | # On Mac OS X, use the try the default path to XCode lldb module 47 | if "darwin" in sys.platform: 48 | xcode_python_path = "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/Current/Resources/Python/" 49 | sys.path.append(xcode_python_path) 50 | try: 51 | import lldb 52 | return True 53 | except ImportError: 54 | # Unable to import lldb module from default Xcode python path 55 | pass 56 | 57 | return False 58 | 59 | if not import_lldb(): 60 | import vim 61 | vim.command('redraw | echo "%s"' % " Error loading lldb module; vim-lldb will be disabled. Check LLDB installation or set LLDB environment variable.") 62 | -------------------------------------------------------------------------------- /python-vim-lldb/lldb_controller.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # This file defines the layer that talks to lldb 4 | # 5 | 6 | import os, re, sys 7 | import lldb 8 | import vim 9 | from vim_ui import UI 10 | 11 | # ================================================= 12 | # Convert some enum value to its string counterpart 13 | # ================================================= 14 | 15 | # Shamelessly copy/pasted from lldbutil.py in the test suite 16 | def state_type_to_str(enum): 17 | """Returns the stateType string given an enum.""" 18 | if enum == lldb.eStateInvalid: 19 | return "invalid" 20 | elif enum == lldb.eStateUnloaded: 21 | return "unloaded" 22 | elif enum == lldb.eStateConnected: 23 | return "connected" 24 | elif enum == lldb.eStateAttaching: 25 | return "attaching" 26 | elif enum == lldb.eStateLaunching: 27 | return "launching" 28 | elif enum == lldb.eStateStopped: 29 | return "stopped" 30 | elif enum == lldb.eStateRunning: 31 | return "running" 32 | elif enum == lldb.eStateStepping: 33 | return "stepping" 34 | elif enum == lldb.eStateCrashed: 35 | return "crashed" 36 | elif enum == lldb.eStateDetached: 37 | return "detached" 38 | elif enum == lldb.eStateExited: 39 | return "exited" 40 | elif enum == lldb.eStateSuspended: 41 | return "suspended" 42 | else: 43 | raise Exception("Unknown StateType enum") 44 | 45 | class StepType: 46 | INSTRUCTION = 1 47 | INSTRUCTION_OVER = 2 48 | INTO = 3 49 | OVER = 4 50 | OUT = 5 51 | 52 | class LLDBController(object): 53 | """ Handles Vim and LLDB events such as commands and lldb events. """ 54 | 55 | # Timeouts (sec) for waiting on new events. Because vim is not multi-threaded, we are restricted to 56 | # servicing LLDB events from the main UI thread. Usually, we only process events that are already 57 | # sitting on the queue. But in some situations (when we are expecting an event as a result of some 58 | # user interaction) we want to wait for it. The constants below set these wait period in which the 59 | # Vim UI is "blocked". Lower numbers will make Vim more responsive, but LLDB will be delayed and higher 60 | # numbers will mean that LLDB events are processed faster, but the Vim UI may appear less responsive at 61 | # times. 62 | eventDelayStep = 2 63 | eventDelayLaunch = 1 64 | eventDelayContinue = 1 65 | 66 | def __init__(self): 67 | """ Creates the LLDB SBDebugger object and initializes the UI class. """ 68 | self.target = None 69 | self.process = None 70 | self.load_dependent_modules = True 71 | 72 | self.dbg = lldb.SBDebugger.Create() 73 | self.commandInterpreter = self.dbg.GetCommandInterpreter() 74 | self.commandInterpreter.HandleCommand("settings set target.load-script-from-symbol-file false", lldb.SBCommandReturnObject()) 75 | 76 | self.ui = UI() 77 | 78 | def completeCommand(self, a, l, p): 79 | """ Returns a list of viable completions for command a with length l and cursor at p """ 80 | 81 | assert l[0] == 'L' 82 | # Remove first 'L' character that all commands start with 83 | l = l[1:] 84 | 85 | # Adjust length as string has 1 less character 86 | p = int(p) - 1 87 | 88 | result = lldb.SBStringList() 89 | num = self.commandInterpreter.HandleCompletion(l, p, 1, -1, result) 90 | 91 | if num == -1: 92 | # FIXME: insert completion character... what's a completion character? 93 | pass 94 | elif num == -2: 95 | # FIXME: replace line with result.GetStringAtIndex(0) 96 | pass 97 | 98 | if result.GetSize() > 0: 99 | results = filter(None, [result.GetStringAtIndex(x) for x in range(result.GetSize())]) 100 | return results 101 | else: 102 | return [] 103 | 104 | def doStep(self, stepType): 105 | """ Perform a step command and block the UI for eventDelayStep seconds in order to process 106 | events on lldb's event queue. 107 | FIXME: if the step does not complete in eventDelayStep seconds, we relinquish control to 108 | the main thread to avoid the appearance of a "hang". If this happens, the UI will 109 | update whenever; usually when the user moves the cursor. This is somewhat annoying. 110 | """ 111 | if not self.process: 112 | sys.stderr.write("No process to step") 113 | return 114 | 115 | t = self.process.GetSelectedThread() 116 | if stepType == StepType.INSTRUCTION: 117 | t.StepInstruction(False) 118 | if stepType == StepType.INSTRUCTION_OVER: 119 | t.StepInstruction(True) 120 | elif stepType == StepType.INTO: 121 | t.StepInto() 122 | elif stepType == StepType.OVER: 123 | t.StepOver() 124 | elif stepType == StepType.OUT: 125 | t.StepOut() 126 | 127 | self.processPendingEvents(self.eventDelayStep, True) 128 | 129 | def doSelect(self, command, args): 130 | """ Like doCommand, but suppress output when "select" is the first argument.""" 131 | a = args.split(' ') 132 | return self.doCommand(command, args, "select" != a[0], True) 133 | 134 | def doProcess(self, args): 135 | """ Handle 'process' command. If 'launch' is requested, use doLaunch() instead 136 | of the command interpreter to start the inferior process. 137 | """ 138 | a = args.split(' ') 139 | if len(args) == 0 or (len(a) > 0 and a[0] != 'launch'): 140 | self.doCommand("process", args) 141 | #self.ui.update(self.target, "", self) 142 | else: 143 | self.doLaunch('-s' not in args, "") 144 | 145 | def doAttach(self, process_name): 146 | """ Handle process attach. """ 147 | error = lldb.SBError() 148 | 149 | self.processListener = lldb.SBListener("process_event_listener") 150 | self.target = self.dbg.CreateTarget('') 151 | self.process = self.target.AttachToProcessWithName(self.processListener, process_name, False, error) 152 | if not error.Success(): 153 | sys.stderr.write("Error during attach: " + str(error)) 154 | return 155 | 156 | self.ui.activate() 157 | self.pid = self.process.GetProcessID() 158 | 159 | print "Attached to %s (pid=%d)" % (process_name, self.pid) 160 | 161 | def doDetach(self): 162 | if self.process is not None and self.process.IsValid(): 163 | pid = self.process.GetProcessID() 164 | state = state_type_to_str(self.process.GetState()) 165 | self.process.Detach() 166 | self.processPendingEvents(self.eventDelayLaunch) 167 | 168 | def doLaunch(self, stop_at_entry, args): 169 | """ Handle process launch. """ 170 | error = lldb.SBError() 171 | 172 | fs = self.target.GetExecutable() 173 | exe = os.path.join(fs.GetDirectory(), fs.GetFilename()) 174 | if self.process is not None and self.process.IsValid(): 175 | pid = self.process.GetProcessID() 176 | state = state_type_to_str(self.process.GetState()) 177 | self.process.Destroy() 178 | 179 | launchInfo = lldb.SBLaunchInfo(args.split(' ')) 180 | self.process = self.target.Launch(launchInfo, error) 181 | if not error.Success(): 182 | sys.stderr.write("Error during launch: " + str(error)) 183 | return 184 | 185 | # launch succeeded, store pid and add some event listeners 186 | self.pid = self.process.GetProcessID() 187 | self.processListener = lldb.SBListener("process_event_listener") 188 | self.process.GetBroadcaster().AddListener(self.processListener, lldb.SBProcess.eBroadcastBitStateChanged) 189 | 190 | print "Launched %s %s (pid=%d)" % (exe, args, self.pid) 191 | 192 | if not stop_at_entry: 193 | self.doContinue() 194 | else: 195 | self.processPendingEvents(self.eventDelayLaunch) 196 | 197 | def doTarget(self, args): 198 | """ Pass target command to interpreter, except if argument is not one of the valid options, or 199 | is create, in which case try to create a target with the argument as the executable. For example: 200 | target list ==> handled by interpreter 201 | target create blah ==> custom creation of target 'blah' 202 | target blah ==> also creates target blah 203 | """ 204 | target_args = [#"create", 205 | "delete", 206 | "list", 207 | "modules", 208 | "select", 209 | "stop-hook", 210 | "symbols", 211 | "variable"] 212 | 213 | a = args.split(' ') 214 | if len(args) == 0 or (len(a) > 0 and a[0] in target_args): 215 | self.doCommand("target", args) 216 | return 217 | elif len(a) > 1 and a[0] == "create": 218 | exe = a[1] 219 | elif len(a) == 1 and a[0] not in target_args: 220 | exe = a[0] 221 | 222 | err = lldb.SBError() 223 | self.target = self.dbg.CreateTarget(exe, None, None, self.load_dependent_modules, err) 224 | if not self.target: 225 | sys.stderr.write("Error creating target %s. %s" % (str(exe), str(err))) 226 | return 227 | 228 | self.ui.activate() 229 | self.ui.update(self.target, "created target %s" % str(exe), self) 230 | 231 | def doContinue(self): 232 | """ Handle 'contiue' command. 233 | FIXME: switch to doCommand("continue", ...) to handle -i ignore-count param. 234 | """ 235 | if not self.process or not self.process.IsValid(): 236 | sys.stderr.write("No process to continue") 237 | return 238 | 239 | self.process.Continue() 240 | self.processPendingEvents(self.eventDelayContinue) 241 | 242 | def doBreakpoint(self, args): 243 | """ Handle breakpoint command with command interpreter, except if the user calls 244 | "breakpoint" with no other args, in which case add a breakpoint at the line 245 | under the cursor. 246 | """ 247 | a = args.split(' ') 248 | if len(args) == 0: 249 | show_output = False 250 | 251 | # User called us with no args, so toggle the bp under cursor 252 | cw = vim.current.window 253 | cb = vim.current.buffer 254 | name = cb.name 255 | line = cw.cursor[0] 256 | 257 | # Since the UI is responsbile for placing signs at bp locations, we have to 258 | # ask it if there already is one or more breakpoints at (file, line)... 259 | if self.ui.haveBreakpoint(name, line): 260 | bps = self.ui.getBreakpoints(name, line) 261 | args = "delete %s" % " ".join([str(b.GetID()) for b in bps]) 262 | self.ui.deleteBreakpoints(name, line) 263 | else: 264 | args = "set -f %s -l %d" % (name, line) 265 | else: 266 | show_output = True 267 | 268 | self.doCommand("breakpoint", args, show_output) 269 | return 270 | 271 | def doRefresh(self): 272 | """ process pending events and update UI on request """ 273 | status = self.processPendingEvents() 274 | 275 | def doShow(self, name): 276 | """ handle :Lshow """ 277 | if not name: 278 | self.ui.activate() 279 | return 280 | 281 | if self.ui.showWindow(name): 282 | self.ui.update(self.target, "", self) 283 | 284 | def doHide(self, name): 285 | """ handle :Lhide """ 286 | if self.ui.hideWindow(name): 287 | self.ui.update(self.target, "", self) 288 | 289 | def doExit(self): 290 | self.dbg.Terminate() 291 | self.dbg = None 292 | 293 | def getCommandResult(self, command, command_args): 294 | """ Run cmd in the command interpreter and returns (success, output) """ 295 | result = lldb.SBCommandReturnObject() 296 | cmd = "%s %s" % (command, command_args) 297 | 298 | self.commandInterpreter.HandleCommand(cmd, result) 299 | return (result.Succeeded(), result.GetOutput() if result.Succeeded() else result.GetError()) 300 | 301 | def doCommand(self, command, command_args, print_on_success = True, goto_file=False): 302 | """ Run cmd in interpreter and print result (success or failure) on the vim status line. """ 303 | (success, output) = self.getCommandResult(command, command_args) 304 | if success: 305 | self.ui.update(self.target, "", self, goto_file) 306 | if len(output) > 0 and print_on_success: 307 | print output 308 | else: 309 | sys.stderr.write(output) 310 | 311 | def getCommandOutput(self, command, command_args=""): 312 | """ runs cmd in the command interpreter andreturns (status, result) """ 313 | result = lldb.SBCommandReturnObject() 314 | cmd = "%s %s" % (command, command_args) 315 | self.commandInterpreter.HandleCommand(cmd, result) 316 | return (result.Succeeded(), result.GetOutput() if result.Succeeded() else result.GetError()) 317 | 318 | def processPendingEvents(self, wait_seconds=0, goto_file=True): 319 | """ Handle any events that are queued from the inferior. 320 | Blocks for at most wait_seconds, or if wait_seconds == 0, 321 | process only events that are already queued. 322 | """ 323 | 324 | status = None 325 | num_events_handled = 0 326 | 327 | if self.process is not None: 328 | event = lldb.SBEvent() 329 | old_state = self.process.GetState() 330 | new_state = None 331 | done = False 332 | if old_state == lldb.eStateInvalid or old_state == lldb.eStateExited: 333 | # Early-exit if we are in 'boring' states 334 | pass 335 | else: 336 | while not done and self.processListener is not None: 337 | if not self.processListener.PeekAtNextEvent(event): 338 | if wait_seconds > 0: 339 | # No events on the queue, but we are allowed to wait for wait_seconds 340 | # for any events to show up. 341 | self.processListener.WaitForEvent(wait_seconds, event) 342 | new_state = lldb.SBProcess.GetStateFromEvent(event) 343 | 344 | num_events_handled += 1 345 | 346 | done = not self.processListener.PeekAtNextEvent(event) 347 | else: 348 | # An event is on the queue, process it here. 349 | self.processListener.GetNextEvent(event) 350 | new_state = lldb.SBProcess.GetStateFromEvent(event) 351 | 352 | # continue if stopped after attaching 353 | if old_state == lldb.eStateAttaching and new_state == lldb.eStateStopped: 354 | self.process.Continue() 355 | 356 | # If needed, perform any event-specific behaviour here 357 | num_events_handled += 1 358 | 359 | if num_events_handled == 0: 360 | pass 361 | else: 362 | if old_state == new_state: 363 | status = "" 364 | self.ui.update(self.target, status, self, goto_file) 365 | 366 | 367 | def returnCompleteCommand(a, l, p): 368 | """ Returns a "\n"-separated string with possible completion results 369 | for command a with length l and cursor at p. 370 | """ 371 | separator = "\n" 372 | results = ctrl.completeCommand(a, l, p) 373 | vim.command('return "%s%s"' % (separator.join(results), separator)) 374 | 375 | def returnCompleteWindow(a, l, p): 376 | """ Returns a "\n"-separated string with possible completion results 377 | for commands that expect a window name parameter (like hide/show). 378 | FIXME: connect to ctrl.ui instead of hardcoding the list here 379 | """ 380 | separator = "\n" 381 | results = ['breakpoints', 'backtrace', 'disassembly', 'locals', 'threads', 'registers'] 382 | vim.command('return "%s%s"' % (separator.join(results), separator)) 383 | 384 | global ctrl 385 | ctrl = LLDBController() 386 | -------------------------------------------------------------------------------- /python-vim-lldb/plugin.py: -------------------------------------------------------------------------------- 1 | 2 | # Try to import all dependencies, catch and handle the error gracefully if it fails. 3 | 4 | import import_lldb 5 | 6 | try: 7 | import lldb 8 | import vim 9 | except ImportError: 10 | 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") 11 | pass 12 | else: 13 | # Everthing went well, so use import to start the plugin controller 14 | from lldb_controller import * 15 | -------------------------------------------------------------------------------- /python-vim-lldb/vim_panes.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file contains implementations of the LLDB display panes in VIM 3 | # 4 | # The most generic way to define a new window is to inherit from VimPane 5 | # and to implement: 6 | # - get_content() - returns a string with the pane contents 7 | # 8 | # Optionally, to highlight text, implement: 9 | # - get_highlights() - returns a map 10 | # 11 | # And call: 12 | # - define_highlight(unique_name, colour) 13 | # at some point in the constructor. 14 | # 15 | # 16 | # If the pane shows some key-value data that is in the context of a 17 | # single frame, inherit from FrameKeyValuePane and implement: 18 | # - get_frame_content(self, SBFrame frame) 19 | # 20 | # 21 | # If the pane presents some information that can be retrieved with 22 | # a simple LLDB command while the subprocess is stopped, inherit 23 | # from StoppedCommandPane and call: 24 | # - self.setCommand(command, command_args) 25 | # at some point in the constructor. 26 | # 27 | # Optionally, you can implement: 28 | # - get_selected_line() 29 | # to highlight a selected line and place the cursor there. 30 | # 31 | # 32 | # FIXME: implement WatchlistPane to displayed watched expressions 33 | # FIXME: define interface for interactive panes, like catching enter 34 | # presses to change selected frame/thread... 35 | # 36 | 37 | import lldb 38 | import vim 39 | 40 | import sys 41 | 42 | # ============================================================== 43 | # Get the description of an lldb object or None if not available 44 | # ============================================================== 45 | 46 | # Shamelessly copy/pasted from lldbutil.py in the test suite 47 | def get_description(obj, option=None): 48 | """Calls lldb_obj.GetDescription() and returns a string, or None. 49 | 50 | For SBTarget, SBBreakpointLocation, and SBWatchpoint lldb objects, an extra 51 | option can be passed in to describe the detailed level of description 52 | desired: 53 | o lldb.eDescriptionLevelBrief 54 | o lldb.eDescriptionLevelFull 55 | o lldb.eDescriptionLevelVerbose 56 | """ 57 | method = getattr(obj, 'GetDescription') 58 | if not method: 59 | return None 60 | tuple = (lldb.SBTarget, lldb.SBBreakpointLocation, lldb.SBWatchpoint) 61 | if isinstance(obj, tuple): 62 | if option is None: 63 | option = lldb.eDescriptionLevelBrief 64 | 65 | stream = lldb.SBStream() 66 | if option is None: 67 | success = method(stream) 68 | else: 69 | success = method(stream, option) 70 | if not success: 71 | return None 72 | return stream.GetData() 73 | 74 | def get_selected_thread(target): 75 | """ Returns a tuple with (thread, error) where thread == None if error occurs """ 76 | process = target.GetProcess() 77 | if process is None or not process.IsValid(): 78 | return (None, VimPane.MSG_NO_PROCESS) 79 | 80 | thread = process.GetSelectedThread() 81 | if thread is None or not thread.IsValid(): 82 | return (None, VimPane.MSG_NO_THREADS) 83 | return (thread, "") 84 | 85 | def get_selected_frame(target): 86 | """ Returns a tuple with (frame, error) where frame == None if error occurs """ 87 | (thread, error) = get_selected_thread(target) 88 | if thread is None: 89 | return (None, error) 90 | 91 | frame = thread.GetSelectedFrame() 92 | if frame is None or not frame.IsValid(): 93 | return (None, VimPane.MSG_NO_FRAME) 94 | return (frame, "") 95 | 96 | def _cmd(cmd): 97 | vim.command("call confirm('%s')" % cmd) 98 | vim.command(cmd) 99 | 100 | def move_cursor(line, col=0): 101 | """ moves cursor to specified line and col """ 102 | cw = vim.current.window 103 | if cw.cursor[0] != line: 104 | vim.command("execute \"normal %dgg\"" % line) 105 | 106 | def winnr(): 107 | """ Returns currently selected window number """ 108 | return int(vim.eval("winnr()")) 109 | 110 | def bufwinnr(name): 111 | """ Returns window number corresponding with buffer name """ 112 | return int(vim.eval("bufwinnr('%s')" % name)) 113 | 114 | def goto_window(nr): 115 | """ go to window number nr""" 116 | if nr != winnr(): 117 | vim.command(str(nr) + ' wincmd w') 118 | 119 | def goto_next_window(): 120 | """ go to next window. """ 121 | vim.command('wincmd w') 122 | return (winnr(), vim.current.buffer.name) 123 | 124 | def goto_previous_window(): 125 | """ go to previously selected window """ 126 | vim.command("execute \"normal \\p\"") 127 | 128 | def have_gui(): 129 | """ Returns True if vim is in a gui (Gvim/MacVim), False otherwise. """ 130 | return int(vim.eval("has('gui_running')")) == 1 131 | 132 | class PaneLayout(object): 133 | """ A container for a (vertical) group layout of VimPanes """ 134 | 135 | def __init__(self): 136 | self.panes = {} 137 | 138 | def havePane(self, name): 139 | """ Returns true if name is a registered pane, False otherwise """ 140 | return name in self.panes 141 | 142 | def prepare(self, panes = []): 143 | """ Draw panes on screen. If empty list is provided, show all. """ 144 | 145 | # If we can't select a window contained in the layout, we are doing a first draw 146 | first_draw = not self.selectWindow(True) 147 | did_first_draw = False 148 | 149 | # Prepare each registered pane 150 | for name in self.panes: 151 | if name in panes or len(panes) == 0: 152 | if first_draw: 153 | # First window in layout will be created with :vsp, and closed later 154 | vim.command(":vsp") 155 | first_draw = False 156 | did_first_draw = True 157 | self.panes[name].prepare() 158 | 159 | if did_first_draw: 160 | # Close the split window 161 | vim.command(":q") 162 | 163 | self.selectWindow(False) 164 | 165 | def contains(self, bufferName = None): 166 | """ Returns True if window with name bufferName is contained in the layout, False otherwise. 167 | If bufferName is None, the currently selected window is checked. 168 | """ 169 | if not bufferName: 170 | bufferName = vim.current.buffer.name 171 | 172 | for p in self.panes: 173 | if bufferName is not None and bufferName.endswith(p): 174 | return True 175 | return False 176 | 177 | def selectWindow(self, select_contained = True): 178 | """ Selects a window contained in the layout (if select_contained = True) and returns True. 179 | If select_contained = False, a window that is not contained is selected. Returns False 180 | if no group windows can be selected. 181 | """ 182 | if select_contained == self.contains(): 183 | # Simple case: we are already selected 184 | return True 185 | 186 | # Otherwise, switch to next window until we find a contained window, or reach the first window again. 187 | first = winnr() 188 | (curnum, curname) = goto_next_window() 189 | 190 | while not select_contained == self.contains(curname) and curnum != first: 191 | (curnum, curname) = goto_next_window() 192 | 193 | return self.contains(curname) == select_contained 194 | 195 | def hide(self, panes = []): 196 | """ Hide panes specified. If empty list provided, hide all. """ 197 | for name in self.panes: 198 | if name in panes or len(panes) == 0: 199 | self.panes[name].destroy() 200 | 201 | def registerForUpdates(self, p): 202 | self.panes[p.name] = p 203 | 204 | def update(self, target, controller): 205 | for name in self.panes: 206 | self.panes[name].update(target, controller) 207 | 208 | 209 | class VimPane(object): 210 | """ A generic base class for a pane that displays stuff """ 211 | CHANGED_VALUE_HIGHLIGHT_NAME_GUI = 'ColorColumn' 212 | CHANGED_VALUE_HIGHLIGHT_NAME_TERM = 'lldb_changed' 213 | CHANGED_VALUE_HIGHLIGHT_COLOUR_TERM = 'darkred' 214 | 215 | SELECTED_HIGHLIGHT_NAME_GUI = 'Cursor' 216 | SELECTED_HIGHLIGHT_NAME_TERM = 'lldb_selected' 217 | SELECTED_HIGHLIGHT_COLOUR_TERM = 'darkblue' 218 | 219 | MSG_NO_TARGET = "Target does not exist." 220 | MSG_NO_PROCESS = "Process does not exist." 221 | MSG_NO_THREADS = "No valid threads." 222 | MSG_NO_FRAME = "No valid frame." 223 | 224 | # list of defined highlights, so we avoid re-defining them 225 | highlightTypes = [] 226 | 227 | def __init__(self, owner, name, open_below=False, height=3): 228 | self.owner = owner 229 | self.name = name 230 | self.buffer = None 231 | self.maxHeight = 20 232 | self.openBelow = open_below 233 | self.height = height 234 | self.owner.registerForUpdates(self) 235 | 236 | def isPrepared(self): 237 | """ check window is OK """ 238 | if self.buffer == None or len(dir(self.buffer)) == 0 or bufwinnr(self.name) == -1: 239 | return False 240 | return True 241 | 242 | def prepare(self, method = 'new'): 243 | """ check window is OK, if not then create """ 244 | if not self.isPrepared(): 245 | self.create(method) 246 | 247 | def on_create(self): 248 | pass 249 | 250 | def destroy(self): 251 | """ destroy window """ 252 | if self.buffer == None or len(dir(self.buffer)) == 0: 253 | return 254 | vim.command('bdelete ' + self.name) 255 | 256 | def create(self, method): 257 | """ create window """ 258 | 259 | if method != 'edit': 260 | belowcmd = "below" if self.openBelow else "" 261 | vim.command('silent %s %s %s' % (belowcmd, method, self.name)) 262 | else: 263 | vim.command('silent %s %s' % (method, self.name)) 264 | 265 | self.window = vim.current.window 266 | 267 | # Set LLDB pane options 268 | vim.command("setlocal buftype=nofile") # Don't try to open a file 269 | vim.command("setlocal noswapfile") # Don't use a swap file 270 | vim.command("setlocal nonumber") # Don't display line numbers 271 | #vim.command("setlocal nowrap") # Don't wrap text 272 | 273 | # Set indentation-based folding up 274 | # Based on: 275 | # http://vim.wikia.com/wiki/Folding_for_plain_text_files_based_on_indentation 276 | vim.command("setlocal foldmethod=expr") 277 | vim.command("setlocal foldexpr=(getline(v:lnum)=~'^$')?-1:((indent(v:lnum)'.indent(v:lnum+1)):indent(v:lnum))") 278 | vim.command("setlocal foldtext=getline(v:foldstart)") 279 | vim.command("setlocal fillchars=fold:\ ") 280 | 281 | # Save some parameters and reference to buffer 282 | self.buffer = vim.current.buffer 283 | self.width = int( vim.eval("winwidth(0)") ) 284 | self.height = int( vim.eval("winheight(0)") ) 285 | 286 | self.on_create() 287 | goto_previous_window() 288 | 289 | def update(self, target, controller): 290 | """ updates buffer contents """ 291 | self.target = target 292 | if not self.isPrepared(): 293 | # Window is hidden, or otherwise not ready for an update 294 | return 295 | 296 | original_cursor = self.window.cursor 297 | 298 | # Select pane 299 | goto_window(bufwinnr(self.name)) 300 | 301 | # Clean and update content, and apply any highlights. 302 | self.clean() 303 | 304 | if self.write(self.get_content(target, controller)): 305 | self.apply_highlights() 306 | 307 | cursor = self.get_selected_line() 308 | if cursor is None: 309 | # Place the cursor at its original position in the window 310 | cursor_line = min(original_cursor[0], len(self.buffer)) 311 | cursor_col = min(original_cursor[1], len(self.buffer[cursor_line - 1])) 312 | else: 313 | # Place the cursor at the location requested by a VimPane implementation 314 | cursor_line = min(cursor, len(self.buffer)) 315 | cursor_col = self.window.cursor[1] 316 | 317 | self.window.cursor = (cursor_line, cursor_col) 318 | 319 | goto_previous_window() 320 | 321 | def get_selected_line(self): 322 | """ Returns the line number to move the cursor to, or None to leave 323 | it where the user last left it. 324 | Subclasses implement this to define custom behaviour. 325 | """ 326 | return None 327 | 328 | def apply_highlights(self): 329 | """ Highlights each set of lines in each highlight group """ 330 | highlights = self.get_highlights() 331 | for highlightType in highlights: 332 | lines = highlights[highlightType] 333 | if len(lines) == 0: 334 | continue 335 | 336 | cmd = 'match %s /' % highlightType 337 | lines = ['\%' + '%d' % line + 'l' for line in lines] 338 | cmd += '\\|'.join(lines) 339 | cmd += '/' 340 | vim.command(cmd) 341 | 342 | def define_highlight(self, name, colour): 343 | """ Defines highlihght """ 344 | if name in VimPane.highlightTypes: 345 | # highlight already defined 346 | return 347 | 348 | vim.command("highlight %s ctermbg=%s guibg=%s" % (name, colour, colour)) 349 | VimPane.highlightTypes.append(name) 350 | 351 | def write(self, msg): 352 | """ replace buffer with msg""" 353 | self.prepare() 354 | 355 | msg = str(msg.encode("utf-8", "replace")).split('\n') 356 | try: 357 | self.buffer.append(msg) 358 | vim.command("execute \"normal ggdd\"") 359 | except vim.error: 360 | # cannot update window; happens when vim is exiting. 361 | return False 362 | 363 | move_cursor(1, 0) 364 | return True 365 | 366 | def clean(self): 367 | """ clean all datas in buffer """ 368 | self.prepare() 369 | vim.command(':%d') 370 | #self.buffer[:] = None 371 | 372 | def get_content(self, target, controller): 373 | """ subclasses implement this to provide pane content """ 374 | assert(0 and "pane subclass must implement this") 375 | pass 376 | 377 | def get_highlights(self): 378 | """ Subclasses implement this to provide pane highlights. 379 | This function is expected to return a map of: 380 | { highlight_name ==> [line_number, ...], ... } 381 | """ 382 | return {} 383 | 384 | 385 | class FrameKeyValuePane(VimPane): 386 | def __init__(self, owner, name, open_below): 387 | """ Initialize parent, define member variables, choose which highlight 388 | to use based on whether or not we have a gui (MacVim/Gvim). 389 | """ 390 | 391 | VimPane.__init__(self, owner, name, open_below) 392 | 393 | # Map-of-maps key/value history { frame --> { variable_name, variable_value } } 394 | self.frameValues = {} 395 | 396 | if have_gui(): 397 | self.changedHighlight = VimPane.CHANGED_VALUE_HIGHLIGHT_NAME_GUI 398 | else: 399 | self.changedHighlight = VimPane.CHANGED_VALUE_HIGHLIGHT_NAME_TERM 400 | self.define_highlight(VimPane.CHANGED_VALUE_HIGHLIGHT_NAME_TERM, 401 | VimPane.CHANGED_VALUE_HIGHLIGHT_COLOUR_TERM) 402 | 403 | def format_pair(self, key, value, changed = False): 404 | """ Formats a key/value pair. Appends a '*' if changed == True """ 405 | marker = '*' if changed else ' ' 406 | return "%s %s = %s\n" % (marker, key, value) 407 | 408 | def get_content(self, target, controller): 409 | """ Get content for a frame-aware pane. Also builds the list of lines that 410 | need highlighting (i.e. changed values.) 411 | """ 412 | if target is None or not target.IsValid(): 413 | return VimPane.MSG_NO_TARGET 414 | 415 | self.changedLines = [] 416 | 417 | (frame, err) = get_selected_frame(target) 418 | if frame is None: 419 | return err 420 | 421 | output = get_description(frame) 422 | lineNum = 1 423 | 424 | # Retrieve the last values displayed for this frame 425 | frameId = get_description(frame.GetBlock()) 426 | if frameId in self.frameValues: 427 | frameOldValues = self.frameValues[frameId] 428 | else: 429 | frameOldValues = {} 430 | 431 | # Read the frame variables 432 | vals = self.get_frame_content(frame) 433 | for (key, value) in vals: 434 | lineNum += 1 435 | if len(frameOldValues) == 0 or (key in frameOldValues and frameOldValues[key] == value): 436 | output += self.format_pair(key, value) 437 | else: 438 | output += self.format_pair(key, value, True) 439 | self.changedLines.append(lineNum) 440 | 441 | # Save values as oldValues 442 | newValues = {} 443 | for (key, value) in vals: 444 | newValues[key] = value 445 | self.frameValues[frameId] = newValues 446 | 447 | return output 448 | 449 | def get_highlights(self): 450 | ret = {} 451 | ret[self.changedHighlight] = self.changedLines 452 | return ret 453 | 454 | class LocalsPane(FrameKeyValuePane): 455 | """ Pane that displays local variables """ 456 | def __init__(self, owner, name = 'locals'): 457 | FrameKeyValuePane.__init__(self, owner, name, open_below=True) 458 | 459 | # FIXME: allow users to customize display of args/locals/statics/scope 460 | self.arguments = True 461 | self.show_locals = True 462 | self.show_statics = False 463 | self.show_in_scope_only = True 464 | 465 | def format_variable(self, var, indent = 0): 466 | """ Returns a list of tuples of strings "(Type) Name", "Value" for SBValue var 467 | and its children 468 | """ 469 | MAX_DEPTH = 6 470 | 471 | if indent > MAX_DEPTH: 472 | return [] 473 | else: 474 | val = var.GetValue() 475 | if val is None: 476 | # If the value is too big, SBValue.GetValue() returns None; replace with ... 477 | val = "..." 478 | 479 | children = [] 480 | if var.GetNumChildren() > 0: 481 | for x in var: 482 | children.extend(self.format_variable(x, indent + 1)) 483 | 484 | return [("%s(%s) %s" % (' ' * indent, var.GetTypeName(), var.GetName()), 485 | "%s" % val)] + children 486 | 487 | def get_frame_content(self, frame): 488 | """ Returns list of key-value pairs of local variables in frame """ 489 | vals = frame.GetVariables(self.arguments, 490 | self.show_locals, 491 | self.show_statics, 492 | self.show_in_scope_only) 493 | 494 | out = [] 495 | for v in [self.format_variable(x) for x in vals]: 496 | out.extend(v) 497 | 498 | return out 499 | 500 | class RegistersPane(FrameKeyValuePane): 501 | """ Pane that displays the contents of registers """ 502 | def __init__(self, owner, name = 'registers'): 503 | FrameKeyValuePane.__init__(self, owner, name, open_below=True) 504 | 505 | def format_register(self, reg): 506 | """ Returns a tuple of strings ("name", "value") for SBRegister reg. """ 507 | name = reg.GetName() 508 | val = reg.GetValue() 509 | if val is None: 510 | val = "..." 511 | return (name, val.strip()) 512 | 513 | def get_frame_content(self, frame): 514 | """ Returns a list of key-value pairs ("name", "value") of registers in frame """ 515 | 516 | result = [] 517 | for register_sets in frame.GetRegisters(): 518 | # hack the register group name into the list of registers... 519 | result.append((" = = %s =" % register_sets.GetName(), "")) 520 | 521 | for reg in register_sets: 522 | result.append(self.format_register(reg)) 523 | return result 524 | 525 | class CommandPane(VimPane): 526 | """ Pane that displays the output of an LLDB command """ 527 | def __init__(self, owner, name, open_below, process_required=True): 528 | VimPane.__init__(self, owner, name, open_below) 529 | self.process_required = process_required 530 | 531 | def setCommand(self, command, args = ""): 532 | self.command = command 533 | self.args = args 534 | 535 | def get_content(self, target, controller): 536 | output = "" 537 | if not target: 538 | output = VimPane.MSG_NO_TARGET 539 | elif self.process_required and not target.GetProcess(): 540 | output = VimPane.MSG_NO_PROCESS 541 | else: 542 | (success, output) = controller.getCommandOutput(self.command, self.args) 543 | return output 544 | 545 | class StoppedCommandPane(CommandPane): 546 | """ Pane that displays the output of an LLDB command when the process is 547 | stopped; otherwise displays process status. This class also implements 548 | highlighting for a single line (to show a single-line selected entity.) 549 | """ 550 | def __init__(self, owner, name, open_below): 551 | """ Initialize parent and define highlight to use for selected line. """ 552 | CommandPane.__init__(self, owner, name, open_below) 553 | if have_gui(): 554 | self.selectedHighlight = VimPane.SELECTED_HIGHLIGHT_NAME_GUI 555 | else: 556 | self.selectedHighlight = VimPane.SELECTED_HIGHLIGHT_NAME_TERM 557 | self.define_highlight(VimPane.SELECTED_HIGHLIGHT_NAME_TERM, 558 | VimPane.SELECTED_HIGHLIGHT_COLOUR_TERM) 559 | 560 | def get_content(self, target, controller): 561 | """ Returns the output of a command that relies on the process being stopped. 562 | If the process is not in 'stopped' state, the process status is returned. 563 | """ 564 | output = "" 565 | if not target or not target.IsValid(): 566 | output = VimPane.MSG_NO_TARGET 567 | elif not target.GetProcess() or not target.GetProcess().IsValid(): 568 | output = VimPane.MSG_NO_PROCESS 569 | elif target.GetProcess().GetState() == lldb.eStateStopped: 570 | (success, output) = controller.getCommandOutput(self.command, self.args) 571 | else: 572 | (success, output) = controller.getCommandOutput("process", "status") 573 | return output 574 | 575 | def get_highlights(self): 576 | """ Highlight the line under the cursor. Users moving the cursor has 577 | no effect on the selected line. 578 | """ 579 | ret = {} 580 | line = self.get_selected_line() 581 | if line is not None: 582 | ret[self.selectedHighlight] = [line] 583 | return ret 584 | return ret 585 | 586 | def get_selected_line(self): 587 | """ Subclasses implement this to control where the cursor (and selected highlight) 588 | is placed. 589 | """ 590 | return None 591 | 592 | class DisassemblyPane(CommandPane): 593 | """ Pane that displays disassembly around PC """ 594 | def __init__(self, owner, name = 'disassembly'): 595 | CommandPane.__init__(self, owner, name, open_below=True) 596 | 597 | # FIXME: let users customize the number of instructions to disassemble 598 | self.setCommand("disassemble", "-c %d -p" % self.maxHeight) 599 | 600 | class ThreadPane(StoppedCommandPane): 601 | """ Pane that displays threads list """ 602 | def __init__(self, owner, name = 'threads'): 603 | StoppedCommandPane.__init__(self, owner, name, open_below=False) 604 | self.setCommand("thread", "list") 605 | 606 | # FIXME: the function below assumes threads are listed in sequential order, 607 | # which turns out to not be the case. Highlighting of selected thread 608 | # will be disabled until this can be fixed. LLDB prints a '*' anyways 609 | # beside the selected thread, so this is not too big of a problem. 610 | # def get_selected_line(self): 611 | # """ Place the cursor on the line with the selected entity. 612 | # Subclasses should override this to customize selection. 613 | # Formula: selected_line = selected_thread_id + 1 614 | # """ 615 | # (thread, err) = get_selected_thread(self.target) 616 | # if thread is None: 617 | # return None 618 | # else: 619 | # return thread.GetIndexID() + 1 620 | 621 | class BacktracePane(StoppedCommandPane): 622 | """ Pane that displays backtrace """ 623 | def __init__(self, owner, name = 'backtrace'): 624 | StoppedCommandPane.__init__(self, owner, name, open_below=False) 625 | self.setCommand("bt", "") 626 | 627 | 628 | def get_selected_line(self): 629 | """ Returns the line number in the buffer with the selected frame. 630 | Formula: selected_line = selected_frame_id + 2 631 | FIXME: the above formula hack does not work when the function return 632 | value is printed in the bt window; the wrong line is highlighted. 633 | """ 634 | 635 | (frame, err) = get_selected_frame(self.target) 636 | if frame is None: 637 | return None 638 | else: 639 | return frame.GetFrameID() + 2 640 | 641 | class BreakpointsPane(CommandPane): 642 | def __init__(self, owner, name = 'breakpoints'): 643 | super(BreakpointsPane, self).__init__(owner, name, open_below=False, process_required=False) 644 | self.setCommand("breakpoint", "list") 645 | -------------------------------------------------------------------------------- /python-vim-lldb/vim_signs.py: -------------------------------------------------------------------------------- 1 | 2 | # Classes responsible for drawing signs in the Vim user interface. 3 | 4 | import vim 5 | 6 | class VimSign(object): 7 | SIGN_TEXT_BREAKPOINT_RESOLVED = "B>" 8 | SIGN_TEXT_BREAKPOINT_UNRESOLVED = "b>" 9 | SIGN_TEXT_PC = "->" 10 | SIGN_HIGHLIGHT_COLOUR_PC = 'darkblue' 11 | 12 | # unique sign id (for ':[sign/highlight] define) 13 | sign_id = 1 14 | 15 | # unique name id (for ':sign place') 16 | name_id = 1 17 | 18 | # Map of {(sign_text, highlight_colour) --> sign_name} 19 | defined_signs = {} 20 | 21 | def __init__(self, sign_text, buffer, line_number, highlight_colour=None): 22 | """ Define the sign and highlight (if applicable) and show the sign. """ 23 | 24 | # Get the sign name, either by defining it, or looking it up in the map of defined signs 25 | key = (sign_text, highlight_colour) 26 | if not key in VimSign.defined_signs: 27 | name = self.define(sign_text, highlight_colour) 28 | else: 29 | name = VimSign.defined_signs[key] 30 | 31 | self.show(name, buffer.number, line_number) 32 | pass 33 | 34 | def define(self, sign_text, highlight_colour): 35 | """ Defines sign and highlight (if highlight_colour is not None). """ 36 | sign_name = "sign%d" % VimSign.name_id 37 | if highlight_colour is None: 38 | vim.command("sign define %s text=%s" % (sign_name, sign_text)) 39 | else: 40 | self.highlight_name = "highlight%d" % VimSign.name_id 41 | vim.command("highlight %s ctermbg=%s guibg=%s" % (self.highlight_name, 42 | highlight_colour, 43 | highlight_colour)) 44 | vim.command("sign define %s text=%s linehl=%s texthl=%s" % (sign_name, 45 | sign_text, 46 | self.highlight_name, 47 | self.highlight_name)) 48 | VimSign.defined_signs[(sign_text, highlight_colour)] = sign_name 49 | VimSign.name_id += 1 50 | return sign_name 51 | 52 | 53 | def show(self, name, buffer_number, line_number): 54 | self.id = VimSign.sign_id 55 | VimSign.sign_id += 1 56 | vim.command("sign place %d name=%s line=%d buffer=%s" % (self.id, name, line_number, buffer_number)) 57 | pass 58 | 59 | def hide(self): 60 | vim.command("sign unplace %d" % self.id) 61 | pass 62 | 63 | class BreakpointSign(VimSign): 64 | def __init__(self, buffer, line_number, is_resolved): 65 | txt = VimSign.SIGN_TEXT_BREAKPOINT_RESOLVED if is_resolved else VimSign.SIGN_TEXT_BREAKPOINT_UNRESOLVED 66 | super(BreakpointSign, self).__init__(txt, buffer, line_number) 67 | 68 | class PCSign(VimSign): 69 | def __init__(self, buffer, line_number, is_selected_thread): 70 | super(PCSign, self).__init__(VimSign.SIGN_TEXT_PC, 71 | buffer, 72 | line_number, 73 | VimSign.SIGN_HIGHLIGHT_COLOUR_PC if is_selected_thread else None) 74 | -------------------------------------------------------------------------------- /python-vim-lldb/vim_ui.py: -------------------------------------------------------------------------------- 1 | 2 | # LLDB UI state in the Vim user interface. 3 | 4 | import os, re, sys 5 | import lldb 6 | import vim 7 | from vim_panes import * 8 | from vim_signs import * 9 | 10 | def is_same_file(a, b): 11 | """ returns true if paths a and b are the same file """ 12 | a = os.path.realpath(a) 13 | b = os.path.realpath(b) 14 | return a in b or b in a 15 | 16 | class UI: 17 | def __init__(self): 18 | """ Declare UI state variables """ 19 | 20 | # Default panes to display 21 | self.defaultPanes = ['breakpoints', 'backtrace', 'locals', 'threads', 'registers', 'disassembly'] 22 | 23 | # map of tuples (filename, line) --> SBBreakpoint 24 | self.markedBreakpoints = {} 25 | 26 | # Currently shown signs 27 | self.breakpointSigns = {} 28 | self.pcSigns = [] 29 | 30 | # Container for panes 31 | self.paneCol = PaneLayout() 32 | 33 | # All possible LLDB panes 34 | self.backtracePane = BacktracePane(self.paneCol) 35 | self.threadPane = ThreadPane(self.paneCol) 36 | self.disassemblyPane = DisassemblyPane(self.paneCol) 37 | self.localsPane = LocalsPane(self.paneCol) 38 | self.registersPane = RegistersPane(self.paneCol) 39 | self.breakPane = BreakpointsPane(self.paneCol) 40 | 41 | def activate(self): 42 | """ Activate UI: display default set of panes """ 43 | self.paneCol.prepare(self.defaultPanes) 44 | 45 | def get_user_buffers(self, filter_name=None): 46 | """ Returns a list of buffers that are not a part of the LLDB UI. That is, they 47 | are not contained in the PaneLayout object self.paneCol. 48 | """ 49 | ret = [] 50 | for w in vim.windows: 51 | b = w.buffer 52 | if not self.paneCol.contains(b.name): 53 | if filter_name is None or filter_name in b.name: 54 | ret.append(b) 55 | return ret 56 | 57 | def update_pc(self, process, buffers, goto_file): 58 | """ Place the PC sign on the PC location of each thread's selected frame """ 59 | 60 | def GetPCSourceLocation(thread): 61 | """ Returns a tuple (thread_index, file, line, column) that represents where 62 | the PC sign should be placed for a thread. 63 | """ 64 | 65 | frame = thread.GetSelectedFrame() 66 | frame_num = frame.GetFrameID() 67 | le = frame.GetLineEntry() 68 | while not le.IsValid() and frame_num < thread.GetNumFrames(): 69 | frame_num += 1 70 | le = thread.GetFrameAtIndex(frame_num).GetLineEntry() 71 | 72 | if le.IsValid(): 73 | path = os.path.join(le.GetFileSpec().GetDirectory(), le.GetFileSpec().GetFilename()) 74 | return (thread.GetIndexID(), path, le.GetLine(), le.GetColumn()) 75 | return None 76 | 77 | 78 | # Clear all existing PC signs 79 | del_list = [] 80 | for sign in self.pcSigns: 81 | sign.hide() 82 | del_list.append(sign) 83 | for sign in del_list: 84 | self.pcSigns.remove(sign) 85 | del sign 86 | 87 | # Select a user (non-lldb) window 88 | if not self.paneCol.selectWindow(False): 89 | # No user window found; avoid clobbering by splitting 90 | vim.command(":vsp") 91 | 92 | # Show a PC marker for each thread 93 | for thread in process: 94 | loc = GetPCSourceLocation(thread) 95 | if not loc: 96 | # no valid source locations for PCs. hide all existing PC markers 97 | continue 98 | 99 | buf = None 100 | (tid, fname, line, col) = loc 101 | buffers = self.get_user_buffers(fname) 102 | is_selected = thread.GetIndexID() == process.GetSelectedThread().GetIndexID() 103 | if len(buffers) == 1: 104 | buf = buffers[0] 105 | if buf != vim.current.buffer: 106 | # Vim has an open buffer to the required file: select it 107 | vim.command('execute ":%db"' % buf.number) 108 | elif is_selected and vim.current.buffer.name not in fname and os.path.exists(fname) and goto_file: 109 | # FIXME: If current buffer is modified, vim will complain when we try to switch away. 110 | # Find a way to detect if the current buffer is modified, and...warn instead? 111 | vim.command('execute ":e %s"' % fname) 112 | buf = vim.current.buffer 113 | elif len(buffers) > 1 and goto_file: 114 | #FIXME: multiple open buffers match PC location 115 | continue 116 | else: 117 | continue 118 | 119 | self.pcSigns.append(PCSign(buf, line, is_selected)) 120 | 121 | if is_selected and goto_file: 122 | # if the selected file has a PC marker, move the cursor there too 123 | curname = vim.current.buffer.name 124 | if curname is not None and is_same_file(curname, fname): 125 | move_cursor(line, 0) 126 | elif move_cursor: 127 | print "FIXME: not sure where to move cursor because %s != %s " % (vim.current.buffer.name, fname) 128 | 129 | def update_breakpoints(self, target, buffers): 130 | """ Decorates buffer with signs corresponding to breakpoints in target. """ 131 | 132 | def GetBreakpointLocations(bp): 133 | """ Returns a list of tuples (resolved, filename, line) where a breakpoint was resolved. """ 134 | if not bp.IsValid(): 135 | sys.stderr.write("breakpoint is invalid, no locations") 136 | return [] 137 | 138 | ret = [] 139 | numLocs = bp.GetNumLocations() 140 | for i in range(numLocs): 141 | loc = bp.GetLocationAtIndex(i) 142 | desc = get_description(loc, lldb.eDescriptionLevelFull) 143 | match = re.search('at\ ([^:]+):([\d]+)', desc) 144 | try: 145 | lineNum = int(match.group(2).strip()) 146 | ret.append((loc.IsResolved(), match.group(1), lineNum)) 147 | except ValueError as e: 148 | sys.stderr.write("unable to parse breakpoint location line number: '%s'" % match.group(2)) 149 | sys.stderr.write(str(e)) 150 | 151 | return ret 152 | 153 | 154 | if target is None or not target.IsValid(): 155 | return 156 | 157 | needed_bps = {} 158 | for bp_index in range(target.GetNumBreakpoints()): 159 | bp = target.GetBreakpointAtIndex(bp_index) 160 | locations = GetBreakpointLocations(bp) 161 | for (is_resolved, file, line) in GetBreakpointLocations(bp): 162 | for buf in buffers: 163 | if file in buf.name: 164 | needed_bps[(buf, line, is_resolved)] = bp 165 | 166 | # Hide any signs that correspond with disabled breakpoints 167 | del_list = [] 168 | for (b, l, r) in self.breakpointSigns: 169 | if (b, l, r) not in needed_bps: 170 | self.breakpointSigns[(b, l, r)].hide() 171 | del_list.append((b, l, r)) 172 | for d in del_list: 173 | del self.breakpointSigns[d] 174 | 175 | # Show any signs for new breakpoints 176 | for (b, l, r) in needed_bps: 177 | bp = needed_bps[(b, l, r)] 178 | if self.haveBreakpoint(b.name, l): 179 | self.markedBreakpoints[(b.name, l)].append(bp) 180 | else: 181 | self.markedBreakpoints[(b.name, l)] = [bp] 182 | 183 | if (b, l, r) not in self.breakpointSigns: 184 | s = BreakpointSign(b, l, r) 185 | self.breakpointSigns[(b, l, r)] = s 186 | 187 | def update(self, target, status, controller, goto_file=False): 188 | """ Updates debugger info panels and breakpoint/pc marks and prints 189 | status to the vim status line. If goto_file is True, the user's 190 | cursor is moved to the source PC location in the selected frame. 191 | """ 192 | 193 | self.paneCol.update(target, controller) 194 | self.update_breakpoints(target, self.get_user_buffers()) 195 | 196 | if target is not None and target.IsValid(): 197 | process = target.GetProcess() 198 | if process is not None and process.IsValid(): 199 | self.update_pc(process, self.get_user_buffers, goto_file) 200 | 201 | if status is not None and len(status) > 0: 202 | print status 203 | 204 | def haveBreakpoint(self, file, line): 205 | """ Returns True if we have a breakpoint at file:line, False otherwise """ 206 | return (file, line) in self.markedBreakpoints 207 | 208 | def getBreakpoints(self, fname, line): 209 | """ Returns the LLDB SBBreakpoint object at fname:line """ 210 | if self.haveBreakpoint(fname, line): 211 | return self.markedBreakpoints[(fname, line)] 212 | else: 213 | return None 214 | 215 | def deleteBreakpoints(self, name, line): 216 | del self.markedBreakpoints[(name, line)] 217 | 218 | def showWindow(self, name): 219 | """ Shows (un-hides) window pane specified by name """ 220 | if not self.paneCol.havePane(name): 221 | sys.stderr.write("unknown window: %s" % name) 222 | return False 223 | self.paneCol.prepare([name]) 224 | return True 225 | 226 | def hideWindow(self, name): 227 | """ Hides window pane specified by name """ 228 | if not self.paneCol.havePane(name): 229 | sys.stderr.write("unknown window: %s" % name) 230 | return False 231 | self.paneCol.hide([name]) 232 | return True 233 | 234 | global ui 235 | ui = UI() 236 | -------------------------------------------------------------------------------- /vim-lldb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilligan/vim-lldb/f4525e12edef82c116a6a7e1d9229ac5364fb279/vim-lldb.png --------------------------------------------------------------------------------