├── .gitignore ├── .gitmodules ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Main.sublime-menu ├── Readme.md ├── SublimeAnarchyDebug.sublime-settings ├── atdebug.sublime-commands ├── debug.py ├── images ├── breakpoint_disabled.png ├── breakpoint_enabled.png ├── icons.psd └── stop_point.png ├── lldb_console.py ├── lldb_console.sublime-syntax ├── lldb_stack.sublime-syntax └── messages.json /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lldb_bridge"] 2 | path = lldb_bridge 3 | url = https://github.com/AnarchyTools/lldb-server.git 4 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": [ "ctrl+alt+r" ], 4 | "command": "atdebug", 5 | "args": { 6 | "start": true 7 | } 8 | }, 9 | { 10 | "keys": [ "ctrl+alt+b" ], 11 | "command": "atlldb", 12 | "args": { 13 | "toggle_breakpoint": true 14 | } 15 | }, 16 | { 17 | "keys": [ "ctrl+alt+shift+b" ], 18 | "command": "atlldb", 19 | "args": { 20 | "enable_disable_breakpoint": true 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": [ "super+alt+r" ], 4 | "command": "atdebug", 5 | "args": { 6 | "start": true 7 | } 8 | }, 9 | { 10 | "keys": [ "super+alt+b" ], 11 | "command": "atlldb", 12 | "args": { 13 | "toggle_breakpoint": true 14 | } 15 | }, 16 | { 17 | "keys": [ "super+alt+shift+b" ], 18 | "command": "atlldb", 19 | "args": { 20 | "enable_disable_breakpoint": true 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "mnemonic": "P", 11 | "id": "package-settings", 12 | "children": 13 | [ 14 | { 15 | "caption": "SublimeAnarchyDebug", 16 | "children": 17 | [ 18 | { 19 | "command": "open_file", 20 | "args": {"file": "${packages}/SublimeAnarchyDebug/SublimeAnarchy.sublime-settings"}, 21 | "caption": "Settings – Default" 22 | }, 23 | { 24 | "command": "open_file", 25 | "args": {"file": "${packages}/User/SublimeAnarchyDebug.sublime-settings"}, 26 | "caption": "Settings – User" 27 | }, 28 | { "caption": "-" }, 29 | { 30 | "command": "open_file", 31 | "args": { 32 | "file": "${packages}/SublimeAnarchyDebug/Default (OSX).sublime-keymap", 33 | "platform": "OSX" 34 | }, 35 | "caption": "Key Bindings – Default" 36 | }, 37 | { 38 | "command": "open_file", 39 | "args": { 40 | "file": "${packages}/SublimeAnarchyDebug/Default (Linux).sublime-keymap", 41 | "platform": "Linux" 42 | }, 43 | "caption": "Key Bindings – Default" 44 | }, 45 | { 46 | "command": "open_file", 47 | "args": { 48 | "file": "${packages}/User/Default (OSX).sublime-keymap", 49 | "platform": "OSX" 50 | }, 51 | "caption": "Key Bindings – User" 52 | }, 53 | { 54 | "command": "open_file", 55 | "args": { 56 | "file": "${packages}/User/Default (Linux).sublime-keymap", 57 | "platform": "Linux" 58 | }, 59 | "caption": "Key Bindings – User" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | ] 68 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Sublime Text 3 plugin for LLDB integration 2 | 3 | ## Features 4 | 5 | - Setting breakpoints 6 | - Running with connected stdin/out/err in output panel 7 | - LLDB debug prompt 8 | - Local variable display 9 | - Backtraces 10 | 11 | ## Roadmap 12 | 13 | - Stabilize killing of debug server 14 | - Work out bugs in lldb console show/hide 15 | - Remote debugging 16 | 17 | ## Setup 18 | 19 | Use the default Sublime method of overriding configuration from the menu. 20 | Available configuration options: 21 | 22 | - `lldb_python_path` path to lldb python package directory to use for the debugger 23 | - `auto_show_lldb_console` boolean, automatically show the lldb console and backtrace windows when starting the debugger 24 | 25 | ## How to use 26 | 27 | To use the debugger you have to configure a debug target and its settings. 28 | To keep it with the project we save the settings to the sublime project file. 29 | 30 | Example content of `Project.sublime-project`: 31 | 32 | ``` 33 | { 34 | "folders": [ 35 | { 36 | "path": ".", 37 | } 38 | ] 39 | "settings": { 40 | "SublimeAnarchyDebug": { 41 | "debug": { 42 | "executable": "${project_path}/bin/executable", 43 | "params": [ 44 | ], 45 | "path": [ 46 | ], 47 | "environment": [ 48 | ], 49 | "working_dir": "${project_path}" 50 | } 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | Put that into your project root and use the menu entry `Project->Open Project...` to open the project (or double-click in your filesystem browser or even open with `subl ` from the command line.) 57 | 58 | If the project is open just use the Command Palette to execute some Debug commands (all prefixed with `AnarchyDebug:`). 59 | -------------------------------------------------------------------------------- /SublimeAnarchyDebug.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // Path to the LLDB.framework python resources 3 | "lldb_python_path": "/Library/Developer/Toolchains/swift-latest.xctoolchain/System/Library/PrivateFrameworks/LLDB.framework/Resources/Python", 4 | 5 | // Automatically show lldb console when debugger starts? 6 | "auto_show_lldb_console": true 7 | } -------------------------------------------------------------------------------- /atdebug.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "AnarchyDebug: Start debugger", 4 | "command": "atdebug", 5 | "args": { 6 | "start": true 7 | } 8 | }, 9 | { 10 | "caption": "AnarchyDebug: Stop debugger", 11 | "command": "atdebug", 12 | "args": { 13 | "stop": true 14 | } 15 | }, 16 | { 17 | "caption": "AnarchyDebug: Continue", 18 | "command": "atdebug", 19 | "args": { 20 | "action": "continue" 21 | } 22 | }, 23 | { 24 | "caption": "AnarchyDebug: Pause", 25 | "command": "atdebug", 26 | "args": { 27 | "action": "pause" 28 | } 29 | }, 30 | { 31 | "caption": "AnarchyDebug: Step into", 32 | "command": "atdebug", 33 | "args": { 34 | "action": "step_into" 35 | } 36 | }, 37 | { 38 | "caption": "AnarchyDebug: Step over", 39 | "command": "atdebug", 40 | "args": { 41 | "action": "step_over" 42 | } 43 | }, 44 | { 45 | "caption": "AnarchyDebug: Step out", 46 | "command": "atdebug", 47 | "args": { 48 | "action": "step_out" 49 | } 50 | }, 51 | { 52 | "caption": "AnarchyDebug: Kill target", 53 | "command": "atdebug", 54 | "args": { 55 | "action": "stop" 56 | } 57 | }, 58 | { 59 | "caption": "AnarchyDebug: Toggle Breakpoint", 60 | "command": "atlldb", 61 | "args": { 62 | "toggle_breakpoint": true 63 | } 64 | }, 65 | { 66 | "caption": "AnarchyDebug: Enable/Disable Breakpoint", 67 | "command": "atlldb", 68 | "args": { 69 | "enable_disable_breakpoint": true 70 | } 71 | }, 72 | { 73 | "caption": "AnarchyDebug: Show LLDB console", 74 | "command": "atdebug_console", 75 | "args": { 76 | "show": true 77 | } 78 | }, 79 | { 80 | "caption": "AnarchyDebug: Hide LLDB console", 81 | "command": "atdebug_console", 82 | "args": { 83 | "show": false 84 | } 85 | } 86 | ] -------------------------------------------------------------------------------- /debug.py: -------------------------------------------------------------------------------- 1 | import sublime_plugin 2 | import sublime 3 | import json 4 | import random 5 | import xmlrpc.client 6 | from http.client import CannotSendRequest, ResponseNotReady 7 | import threading 8 | from contextlib import contextmanager 9 | 10 | import os 11 | import xmlrpc.client 12 | from time import sleep 13 | 14 | from subprocess import Popen 15 | from datetime import datetime 16 | 17 | debuggers = {} # key = window.id, value lldb proxy 18 | output_callbacks = {} # key = window.id, value set of callback funcs 19 | status_callbacks = {} # key = window.id, value set of callback funcs 20 | 21 | debug_status = {} 22 | 23 | def plugin_loaded(): 24 | global settings 25 | settings = sublime.load_settings('SublimeAnarchyDebug.sublime-settings') 26 | 27 | def plugin_unloaded(): 28 | for key, debugger in debuggers.items(): 29 | debugger.shutdown_server() 30 | 31 | @contextmanager 32 | def retry(): 33 | while True: 34 | try: 35 | yield 36 | except CannotSendRequest: 37 | sleep(0.2) 38 | continue 39 | except ResponseNotReady: 40 | sleep(0.2) 41 | continue 42 | break 43 | 44 | # lldb query functions 45 | def lldb_update_status(window): 46 | lldb = debuggers[window.id()] 47 | with retry(): 48 | try: 49 | status = lldb.get_status() 50 | except xmlrpc.client.Fault: 51 | status = None 52 | except ConnectionRefusedError: 53 | status = "LLDB exited" 54 | if window.id() not in debug_status: 55 | debug_status[window.id()] = "unknown" 56 | 57 | if status != debug_status[window.id()]: 58 | print("state change", debug_status[window.id()], '->', status) 59 | debug_status[window.id()] = status 60 | for callback in status_callbacks[window.id()]: 61 | try: 62 | callback(window, status) 63 | except Exception as e: 64 | print('Exception', e) 65 | 66 | def lldb_update_console(window): 67 | lldb = debuggers[window.id()] 68 | with retry(): 69 | stdout_buffer = lldb.get_stdout() 70 | if stdout_buffer is not None and len(stdout_buffer) > 0: 71 | for callback in output_callbacks[window.id()]: 72 | try: 73 | callback(window, stdout_buffer) 74 | except Exception: 75 | pass 76 | 77 | 78 | # default callbacks for query functions 79 | def main_output_callback(window, output_buffer): 80 | pass 81 | 82 | def main_status_callback(window, status): 83 | if not status: 84 | for view in window.views(): 85 | view.erase_status('lldb') 86 | return 87 | 88 | lldb = debuggers[window.id()] 89 | for view in window.views(): 90 | view.set_status('lldb', 'LLDB: ' + status) 91 | if status.startswith('stopped') or status.startswith('crashed') or status.startswith('plan_complete'): 92 | update_run_marker(window, lldb=lldb) 93 | if status.startswith('exited'): 94 | lldb.shutdown_server() 95 | 96 | 97 | def debugger_thread(p, port, window): 98 | global settings 99 | sleep(0.5) 100 | 101 | project_settings = window.project_data().get('settings', {}).get('SublimeAnarchyDebug', {}).get('debug', {}) 102 | lldb = xmlrpc.client.ServerProxy('http://localhost:' + str(port), allow_none=True) 103 | 104 | project_path = os.path.dirname(window.project_file_name()) 105 | with retry(): 106 | lldb.prepare( 107 | project_settings.get('executable').replace('${project_path}', project_path), 108 | project_settings.get('params', []), 109 | project_settings.get('environment', None), 110 | project_settings.get('path', None), 111 | project_settings.get('working_dir', project_path).replace('${project_path}', project_path) 112 | ) 113 | debuggers[window.id()] = lldb 114 | status_callbacks[window.id()] = set() 115 | status_callbacks[window.id()].add(main_status_callback) 116 | output_callbacks[window.id()] = set() 117 | output_callbacks[window.id()].add(main_output_callback) 118 | 119 | # start the app 120 | status = "unknown" 121 | while status not in ["stopped,signal", "stopped,breakpoint"]: 122 | with retry(): 123 | status = lldb.get_status() 124 | sleep(0.5) 125 | 126 | if settings.get('auto_show_lldb_console', True): 127 | window.run_command('atdebug_console', { "show": True }) 128 | 129 | # load saved breakpoints 130 | atlldb.load_breakpoints(window, lldb) 131 | 132 | with retry(): 133 | lldb.start() 134 | 135 | # polling loop 136 | debug_status[window.id()] = "stopped,signal" 137 | try: 138 | while True: 139 | sleep(1) 140 | lldb_update_status(window) 141 | lldb_update_console(window) 142 | except Exception as e: 143 | print("exception", e) 144 | except ConnectionRefusedError: 145 | print("LLDB Debug server down") 146 | 147 | _kill_lldb(window) 148 | if p: 149 | p.wait() 150 | 151 | 152 | def _kill_lldb(window): 153 | # so the debug server exited or crashed 154 | if window.id() in debuggers: 155 | del debuggers[window.id()] 156 | if window.id() in debug_status: 157 | del debug_status[window.id()] 158 | if window.id() in status_callbacks: 159 | del status_callbacks[window.id()] 160 | if window.id() in output_callbacks: 161 | del output_callbacks[window.id()] 162 | 163 | for view in window.views(): 164 | view.erase_status('lldb') 165 | update_run_marker(view.window()) 166 | window.run_command('atdebug_console', { "show": False }) 167 | 168 | class atdebug(sublime_plugin.WindowCommand): 169 | 170 | def _start_debugger(self): 171 | self._stop_debugger() 172 | path = os.path.dirname(self.window.project_file_name()) 173 | port = random.randint(12000,13000) 174 | #port = 12345 175 | lldb_server_executable = os.path.join(sublime.packages_path(), "SublimeAnarchyDebug", "lldb_bridge", "lldb_server.py") 176 | args = ['/usr/bin/python', lldb_server_executable, settings.get('lldb_python_path'), str(port)] 177 | p = Popen(args, cwd=path) 178 | #p = None 179 | threading.Thread(target=debugger_thread, name='debugger_thread', args=(p, port, self.window)).start() 180 | 181 | def _stop_debugger(self): 182 | lldb = debuggers.get(self.window.id(), None) 183 | if lldb: 184 | with retry(): 185 | try: 186 | lldb.shutdown_server() 187 | except ConnectionRefusedError: 188 | _kill_lldb(self.window) 189 | 190 | def run(self, *args, **kwargs): 191 | if kwargs.get('start', False): 192 | self._start_debugger() 193 | if kwargs.get('stop', False): 194 | self._stop_debugger() 195 | 196 | action = kwargs.get('action', 'nop') 197 | with retry(): 198 | lldb = debuggers.get(self.window.id(), None) 199 | if not lldb: 200 | return 201 | if action == 'nop': 202 | return 203 | elif action == 'continue': 204 | debug_status[self.window.id()] = "running" 205 | lldb.start() 206 | elif action == 'pause': 207 | lldb.pause() 208 | elif action == 'step_into': 209 | debug_status[self.window.id()] = "stepping" 210 | lldb.step_into() 211 | elif action == 'step_over': 212 | debug_status[self.window.id()] = "stepping" 213 | lldb.step_over() 214 | elif action == 'step_out': 215 | debug_status[self.window.id()] = "stepping" 216 | lldb.step_out() 217 | elif action == 'stop': 218 | lldb.stop() 219 | 220 | update_run_marker(self.window, lldb=lldb) 221 | 222 | def is_enabled(self, *args, **kwargs): 223 | if not self.window.project_file_name(): 224 | return False 225 | 226 | if kwargs.get('start', False) and debuggers.get(self.window.id(), None) == None: 227 | return True 228 | 229 | if kwargs.get('stop', False) and debuggers.get(self.window.id(), None) != None: 230 | return True 231 | 232 | if kwargs.get('action', None) and debuggers.get(self.window.id(), None) != None: 233 | return True 234 | 235 | return False 236 | 237 | 238 | class atlldb(sublime_plugin.TextCommand): 239 | 240 | @staticmethod 241 | def save_breakpoints(window, lldb=None, breakpoints=None): 242 | project_data = window.project_data() 243 | if lldb: 244 | with retry(): 245 | breakpoints = lldb.get_breakpoints() 246 | for bp in breakpoints: 247 | del bp['id'] 248 | if 'settings' not in project_data: 249 | project_data['settings'] = {} 250 | if 'SublimeAnarchyDebug' not in project_data['settings']: 251 | project_data['settings']['SublimeAnarchyDebug'] = {} 252 | project_data['settings']['SublimeAnarchyDebug']['breakpoints'] = breakpoints 253 | window.set_project_data(project_data) 254 | 255 | @staticmethod 256 | def load_breakpoints(window, lldb): 257 | breakpoints = window.project_data().get('settings', {}).get('SublimeAnarchyDebug', {}).get('breakpoints', []) 258 | with retry(): 259 | lldb.delete_all_breakpoints() 260 | for bp in breakpoints: 261 | with retry(): 262 | bp_id = lldb.set_breakpoint(bp['file'], bp['line'], bp['condition'], bp['ignore_count']) 263 | if not bp['enabled']: 264 | with retry(): 265 | lldb.disable_breakpoint(bp_id) 266 | 267 | def _disable_breakpoint(self, lldb, bp): 268 | with retry(): 269 | breakpoints = lldb.get_breakpoints() 270 | for lldb_bp in breakpoints: 271 | if lldb_bp['file'] == bp['file'] and lldb_bp['line'] == bp['line']: 272 | with retry(): 273 | lldb.disable_breakpoint(lldb_bp['id']) 274 | self.save_breakpoints(self.view.window(), lldb=lldb) 275 | 276 | def _enable_breakpoint(self, lldb, bp): 277 | with retry(): 278 | breakpoints = lldb.get_breakpoints() 279 | for lldb_bp in breakpoints: 280 | if lldb_bp['file'] == bp['file'] and lldb_bp['line'] == bp['line']: 281 | with retry(): 282 | lldb.enable_breakpoint(lldb_bp['id']) 283 | self.save_breakpoints(self.view.window(), lldb=lldb) 284 | 285 | def _create_breakpoint(self, lldb, file, line): 286 | with retry(): 287 | lldb.set_breakpoint(file, line, None, 0) 288 | self.save_breakpoints(self.view.window(), lldb=lldb) 289 | 290 | def _remove_breakpoint(self, lldb, bp): 291 | with retry(): 292 | breakpoints = lldb.get_breakpoints() 293 | for lldb_bp in breakpoints: 294 | if lldb_bp['file'] == bp['file'] and lldb_bp['line'] == bp['line']: 295 | with retry(): 296 | lldb.delete_breakpoint(lldb_bp['id']) 297 | self.save_breakpoints(self.view.window(), lldb=lldb) 298 | 299 | def toggle_breakpoint(self, lldb): 300 | breakpoints = self.view.window().project_data().get('settings', {}).get('SublimeAnarchyDebug', {}).get('breakpoints', []) 301 | 302 | cursor = self.view.sel()[0].begin() 303 | row, col = self.view.rowcol(cursor) 304 | 305 | found = [] 306 | new_bps = [] 307 | for bp in breakpoints: 308 | if bp['file'] == self.view.file_name() and bp['line'] == row: 309 | if lldb: 310 | self._remove_breakpoint(lldb, bp) 311 | found.append(bp) 312 | 313 | if len(found) == 0: 314 | breakpoints.append({ 315 | "file": self.view.file_name(), 316 | "line": row, 317 | "enabled": True, 318 | "condition": None, 319 | "ignore_count": 0 320 | }) 321 | if lldb: 322 | self._create_breakpoint(lldb, self.view.file_name(), row) 323 | else: 324 | for bp in found: 325 | breakpoints.remove(bp) 326 | if not lldb: 327 | self.save_breakpoints(self.view.window(), breakpoints=breakpoints) 328 | update_markers(self.view) 329 | 330 | def enable_disable_breakpoint(self, lldb): 331 | breakpoints = self.view.window().project_data().get('settings', {}).get('SublimeAnarchyDebug', {}).get('breakpoints', []) 332 | 333 | cursor = self.view.sel()[0].begin() 334 | row, col = self.view.rowcol(cursor) 335 | 336 | found = False 337 | for bp in breakpoints: 338 | if bp['file'] == self.view.file_name() and bp['line'] == row and bp['enabled'] == True: 339 | bp['enabled'] = False 340 | if lldb: 341 | self._disable_breakpoint(lldb, bp) 342 | found = True 343 | elif bp['file'] == self.view.file_name() and bp['line'] == row and bp['enabled'] == False: 344 | bp['enabled'] = True 345 | if lldb: 346 | self._enable_breakpoint(lldb, bp) 347 | found = True 348 | if not lldb: 349 | self.save_breakpoints(self.view.window(), breakpoints=breakpoints) 350 | update_markers(self.view) 351 | 352 | def run(self, *args, **kwargs): 353 | lldb = debuggers.get(self.view.window().id(), None) 354 | if kwargs.get('toggle_breakpoint', False): 355 | self.toggle_breakpoint(lldb) 356 | if kwargs.get('enable_disable_breakpoint', False): 357 | self.enable_disable_breakpoint(lldb) 358 | 359 | def is_enabled(self, *args, **kwargs): 360 | if "source.swift" in self.view.scope_name(0) and self.view.window().project_file_name(): 361 | return False 362 | 363 | # only show enable/disable when there is a breakpoint 364 | if kwargs.get('enable_disable_breakpoint', False): 365 | breakpoints = self.view.window().project_data().get('settings', {}).get('SublimeAnarchyDebug', {}).get('breakpoints', []) 366 | cursor = self.view.sel()[0].begin() 367 | row, col = self.view.rowcol(cursor) 368 | 369 | new_bps = [] 370 | for bp in breakpoints: 371 | if bp['file'] == self.view.file_name() and bp['line'] == row: 372 | return True 373 | return False 374 | 375 | return True 376 | 377 | class LLDBBreakPointHighlighter(sublime_plugin.EventListener): 378 | 379 | def enable(self, view): 380 | if not view: return False 381 | if "source.swift" not in view.scope_name(0): return False 382 | return True 383 | 384 | def on_activated(self, view): 385 | if not self.enable(view): 386 | return 387 | update_markers(view) 388 | 389 | def update_breakpoint_marker(view): 390 | breakpoints = view.window().project_data().get('settings', {}).get('SublimeAnarchyDebug', {}).get('breakpoints', []) 391 | enabled_markers = [] 392 | disabled_markers = [] 393 | for bp in breakpoints: 394 | if bp['file'] == view.file_name(): 395 | location = view.line(view.text_point(bp['line'], 0)) 396 | if bp['enabled']: 397 | enabled_markers.append(location) 398 | else: 399 | disabled_markers.append(location) 400 | view.add_regions("breakpoint_enabled", enabled_markers, "breakpoint_enabled", "Packages/SublimeAnarchyDebug/images/breakpoint_enabled.png", sublime.HIDDEN) 401 | view.add_regions("breakpoint_disabled", disabled_markers, "breakpoint_disabled", "Packages/SublimeAnarchyDebug/images/breakpoint_disabled.png", sublime.HIDDEN) 402 | 403 | def update_run_marker(window, lldb=None): 404 | if not lldb: 405 | for view in window.views(): 406 | view.erase_regions("run_pointer") 407 | return 408 | 409 | with retry(): 410 | try: 411 | bt = lldb.get_backtrace_for_selected_thread() 412 | if 'bt' not in bt: 413 | for view in window.views(): 414 | view.erase_regions("run_pointer") 415 | return 416 | for frame in bt['bt']: 417 | if 'file' in frame and frame['line'] != 0: 418 | found = False 419 | for view in window.views(): 420 | if view.file_name() == frame['file']: 421 | location = view.line(view.text_point(frame['line'] - 1, 0)) 422 | view.add_regions("run_pointer", [location], "entity.name.class", "Packages/SublimeAnarchyDebug/images/stop_point.png", sublime.DRAW_NO_FILL) 423 | if not view.visible_region().contains(location): 424 | view.show_at_center(location) 425 | if window.active_group() == 0: 426 | window.focus_view(view) 427 | found = True 428 | if not found: 429 | grp = window.active_group() 430 | window.focus_group(0) 431 | view = window.open_file(frame['file'] + ":" + str(frame['line']), sublime.ENCODED_POSITION) 432 | window.focus_group(grp) 433 | location = view.line(view.text_point(frame['line'] - 1, 0)) 434 | view.add_regions("run_pointer", [location], "entity.name.class", "Packages/SublimeAnarchyDebug/images/stop_point.png", sublime.DRAW_NO_FILL) 435 | if not view.visible_region().contains(location): 436 | view.show_at_center(location) 437 | break 438 | except xmlrpc.client.Fault: 439 | for view in window.views(): 440 | view.erase_regions("run_pointer") 441 | 442 | def update_markers(view): 443 | update_breakpoint_marker(view) 444 | 445 | lldb = debuggers.get(view.window().id(), None) 446 | update_run_marker(view.window(), lldb=lldb) 447 | if lldb: 448 | lldb_update_status(view.window()) 449 | -------------------------------------------------------------------------------- /images/breakpoint_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnarchyTools/SublimeAnarchyDebug/82a1b30d2ff72fa94a96b616c18aba1e952a7c82/images/breakpoint_disabled.png -------------------------------------------------------------------------------- /images/breakpoint_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnarchyTools/SublimeAnarchyDebug/82a1b30d2ff72fa94a96b616c18aba1e952a7c82/images/breakpoint_enabled.png -------------------------------------------------------------------------------- /images/icons.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnarchyTools/SublimeAnarchyDebug/82a1b30d2ff72fa94a96b616c18aba1e952a7c82/images/icons.psd -------------------------------------------------------------------------------- /images/stop_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnarchyTools/SublimeAnarchyDebug/82a1b30d2ff72fa94a96b616c18aba1e952a7c82/images/stop_point.png -------------------------------------------------------------------------------- /lldb_console.py: -------------------------------------------------------------------------------- 1 | import sublime_plugin 2 | import sublime 3 | 4 | import os 5 | 6 | from .debug import debuggers, status_callbacks, output_callbacks, retry, debug_status 7 | 8 | window_layouts = {} 9 | 10 | def update_stack(window, status): 11 | if window.id() not in debuggers: 12 | return 13 | 14 | lldb = debuggers[window.id()] 15 | if not lldb: 16 | return 17 | 18 | view = None 19 | for v in window.views(): 20 | if v.name() == "LLDB Stack": 21 | view = v 22 | break 23 | if not view: 24 | return 25 | 26 | 27 | with retry(): 28 | bt = lldb.get_backtrace() 29 | 30 | threads = [] 31 | var_dump = "" 32 | for thread_id, info in bt.items(): 33 | buf = "" 34 | 35 | frames = info['bt'] 36 | buf += "* Thread {} ({}, queue: {}, id: {})\n".format(info['index'], info['name'], info['queue'], info['id']) 37 | delim_len = (len(buf) - 1) 38 | buf += "-" * delim_len + "\n" 39 | 40 | max_len = 0 41 | for frame in frames: 42 | if frame['module'] is not None and len(frame['module']) > max_len: 43 | max_len = len(frame['module']) 44 | 45 | frame_id = 0 46 | toplevel = -1 47 | for frame in frames: 48 | if 'function' in frame: 49 | if toplevel < 0: 50 | toplevel = frame_id 51 | f = os.path.relpath(frame['file'], start=os.path.dirname(window.project_file_name())) 52 | buf += '{num: <3} {mod: <{max_len}} {addr:#016x} {file}:{line}'.format( 53 | num=frame_id, 54 | addr=int(frame['address']), # number to big for rpc so this comes as a string -.- 55 | mod=frame['module'], 56 | file=f, 57 | line=frame['line'], 58 | max_len = max_len 59 | ) 60 | if frame['column'] > 0: 61 | buf += ":{col}".format(col=frame['column']) 62 | buf += " ({func})".format(func='%s [inlined]' % frame['function'] if frame['inlined'] else frame['function']) 63 | else: 64 | buf += '{num: <3} {mod: <{max_len}} {addr:#016x} {symbol} + {offset}'.format( 65 | num=frame_id, 66 | addr=int(frame['address']), # number to big for rpc so this comes as a string -.- 67 | mod=frame['module'], 68 | symbol=frame['symbol'], 69 | offset=int(frame['offset']), # number to big for rpc so this comes as a string -.- 70 | max_len=max_len 71 | ) 72 | buf += "\n" 73 | frame_id += 1 74 | 75 | buf += "-" * delim_len + "\n" 76 | buf += "Status: {}".format(info['stop_reason']) 77 | buf += "\n" 78 | 79 | if info['selected']: 80 | with retry(): 81 | var = lldb.get_local_variables(info['id'], toplevel) 82 | var_dump = "* Local variables for frame #{}\n".format(toplevel) 83 | var_dump += "-" * (len(var_dump) - 1) + "\n" 84 | max_len_var = 0 85 | for name, value in var.items(): 86 | if len(name) > max_len_var: 87 | max_len_var = len(name) 88 | items = var.items() 89 | items = sorted(items, key=lambda item: item[0]) 90 | for name, value in items: 91 | var_dump += "{name: >{max_len}} -> {value}\n".format( 92 | name=name, 93 | value=value, 94 | max_len=max_len_var 95 | ) 96 | var_dump += "\n" 97 | threads.insert(0, buf) 98 | else: 99 | threads.append(buf) 100 | 101 | buttons = "[ continue ] [ pause ] [ step into ] [ step over ] [ step out ] [ stop ]\n\n" 102 | if len(threads) > 1: 103 | data = buttons + threads[0] + "\n" + var_dump + "\n".join(threads[1:]) 104 | else: 105 | data = buttons + threads[0] + "\n" + var_dump 106 | view.run_command("update_lldb_stack", { "data": data }) 107 | 108 | def update_console(window, buf): 109 | view = None 110 | for v in window.views(): 111 | if v.name() == "LLDB Console": 112 | view = v 113 | break 114 | if not view: 115 | return 116 | 117 | buf = "\n".join(["STDOUT: " + line for line in buf.strip().split("\n")]) 118 | view.run_command("update_lldb_console", { "data": buf + "\n" }) 119 | 120 | 121 | class updateLldbConsole(sublime_plugin.TextCommand): 122 | 123 | def run(self, edit, **kwargs): 124 | data = kwargs.get("data", "") 125 | last_line = self.view.line(self.view.size()) 126 | line = self.view.substr(last_line) 127 | if line.startswith('(lldb)'): 128 | self.view.replace(edit, last_line, data) 129 | self.view.insert(edit, self.view.size(), line) 130 | else: 131 | self.view.insert(edit, self.view.size(), data) 132 | if len(data) > 0 and data[-1] != "\n": 133 | self.view.insert(edit, self.view.size(), "\n(lldb) ") 134 | else: 135 | self.view.insert(edit, self.view.size(), "(lldb) ") 136 | 137 | 138 | self.view.sel().clear() 139 | self.view.sel().add(sublime.Region(self.view.size(), self.view.size())) 140 | self.view.show(self.view.size(), False) 141 | 142 | def is_visible(self): 143 | return False 144 | 145 | class updateLldbStack(sublime_plugin.TextCommand): 146 | 147 | def run(self, edit, **kwargs): 148 | data = kwargs.get("data", "") 149 | region = sublime.Region(0, self.view.size()) 150 | self.view.replace(edit, region, data) 151 | self.view.sel().clear() 152 | self.view.set_syntax_file('Packages/SublimeAnarchyDebug/lldb_stack.sublime-syntax') 153 | 154 | def is_visible(self): 155 | return False 156 | 157 | class atdebugConsole(sublime_plugin.WindowCommand): 158 | 159 | def _show_console(self): 160 | window_layouts[self.window.id()] = self.window.get_layout() 161 | self.window.set_layout({ 162 | "cols": [0, 0.5, 1], 163 | "rows": [0, 0.5, 1], 164 | "cells": [[0, 0, 1, 2], [1, 0, 2, 1], 165 | [1, 1, 2, 2]] 166 | }) 167 | 168 | view = None 169 | for v in self.window.views(): 170 | if v.name() == "LLDB Stack": 171 | view = v 172 | break 173 | if not view: 174 | self.window.focus_group(1) 175 | view = self.window.new_file() 176 | view.set_scratch(True) 177 | view.set_name('LLDB Stack') 178 | view.set_syntax_file('Packages/SublimeAnarchyDebug/lldb_stack.sublime-syntax') 179 | status_callbacks[self.window.id()].add(update_stack) 180 | 181 | view = None 182 | for v in self.window.views(): 183 | if v.name() == "LLDB Console": 184 | view = v 185 | break 186 | if not view: 187 | self.window.focus_group(2) 188 | view = self.window.new_file() 189 | view.set_scratch(True) 190 | view.set_name('LLDB Console') 191 | view.set_syntax_file('Packages/SublimeAnarchyDebug/lldb_console.sublime-syntax') 192 | view.run_command("update_lldb_console", { "data": "" }) 193 | 194 | output_callbacks[self.window.id()].add(update_console) 195 | self.window.focus_group(0) 196 | 197 | def _hide_console(self): 198 | for view in self.window.views(): 199 | if view.name() in ["LLDB Console", "LLDB Stack"]: 200 | self.window.focus_view(view) 201 | self.window.run_command("close_file") 202 | self.window.set_layout(window_layouts[self.window.id()]) 203 | if self.window.id() in status_callbacks: 204 | status_callbacks[self.window.id()].discard(update_stack) 205 | if self.window.id() in output_callbacks: 206 | output_callbacks[self.window.id()].discard(update_console) 207 | window_layouts.pop(self.window.id(), None) 208 | 209 | def run(self, *args, **kwargs): 210 | if kwargs.get('show', False): 211 | self._show_console() 212 | else: 213 | self._hide_console() 214 | 215 | def is_enabled(self, *args, **kwargs): 216 | if not self.window.project_file_name(): 217 | return False 218 | 219 | if kwargs.get('show', False) and debuggers.get(self.window.id(), None) != None: 220 | return True 221 | 222 | if not kwargs.get('show', False) and window_layouts.get(self.window.id(), None) != None: 223 | return True 224 | 225 | return False 226 | 227 | class LldbConsoleWatcher(sublime_plugin.EventListener): 228 | 229 | def enable(self, view): 230 | if not view: return False 231 | if "lldb.console" not in view.scope_name(0): return False 232 | return True 233 | 234 | def on_activated(self, view): 235 | if not self.enable(view): 236 | return 237 | 238 | view.sel().clear() 239 | view.sel().add(sublime.Region(view.size(), view.size())) 240 | 241 | def on_selection_modified_async(self, view): 242 | if not self.enable(view): 243 | return 244 | 245 | if not view.window().id() in debuggers: 246 | return 247 | 248 | lldb = debuggers[view.window().id()] 249 | 250 | last_line = view.line(view.size()) 251 | line = view.substr(last_line) 252 | if line == "": 253 | last_line = view.line(view.size() - 1) 254 | line = view.substr(last_line) 255 | if line.startswith('(lldb) '): 256 | command = line[7:] 257 | with retry(): 258 | result = lldb.execute_lldb_command(command) 259 | debug_status[view.window().id()] = "command" 260 | if result['succeeded']: 261 | lines = result['output'].split('\n') 262 | buf = "\n".join(["LLDB OK: " + l for l in lines if len(l) > 0]) 263 | view.run_command("update_lldb_console", { "data": buf }) 264 | else: 265 | lines = result['error'].split('\n') 266 | buf = "\n".join(["LLDB ERR: " + l for l in lines if len(l) > 0]) 267 | view.run_command("update_lldb_console", { "data": buf }) 268 | 269 | class LldbStackWatcher(sublime_plugin.EventListener): 270 | 271 | def enable(self, view): 272 | if not view: return False 273 | if "lldb.stack" not in view.scope_name(0): return False 274 | return True 275 | 276 | def on_activated(self, view): 277 | if not self.enable(view): 278 | return 279 | 280 | view.sel().clear() 281 | view.sel().add(view.text_point(1,0)) 282 | 283 | def on_selection_modified_async(self, view): 284 | if not self.enable(view): 285 | return 286 | 287 | if not view.window().id() in debuggers: 288 | return 289 | 290 | if len(view.sel()) == 0: 291 | return 292 | 293 | scope = view.scope_name(view.sel()[0].begin()) 294 | 295 | if 'btn_continue' in scope: 296 | view.window().run_command('atdebug', { 'action' : 'continue' }) 297 | elif 'btn_pause' in scope: 298 | view.window().run_command('atdebug', { 'action' : 'pause' }) 299 | elif 'btn_step_into' in scope: 300 | view.window().run_command('atdebug', { 'action' : 'step_into' }) 301 | elif 'btn_step_over' in scope: 302 | view.window().run_command('atdebug', { 'action' : 'step_over' }) 303 | elif 'btn_step_out' in scope: 304 | view.window().run_command('atdebug', { 'action' : 'step_out' }) 305 | elif 'btn_stop' in scope: 306 | view.window().run_command('atdebug', { 'action' : 'stop' }) 307 | 308 | view.sel().clear() 309 | view.sel().add(view.text_point(1,0)) 310 | -------------------------------------------------------------------------------- /lldb_console.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | name: AnarchyTools LLDB Console 4 | scope: lldb.console 5 | hidden: true 6 | 7 | contexts: 8 | main: 9 | - match: (STDOUT:)\s+(.*) 10 | captures: 11 | 1: comment 12 | 2: string 13 | - match: (STDERR:)\s+(.*) 14 | captures: 15 | 1: comment 16 | 2: keyword 17 | 18 | - match: (LLDB OK:)\s+(.*) 19 | captures: 20 | 1: comment 21 | 2: support.function 22 | 23 | - match: (LLDB ERR:)\s+(.*) 24 | captures: 25 | 1: comment 26 | 2: variable.parameter 27 | 28 | - match: (\(lldb\))\s+(.*) 29 | captures: 30 | 1: keyword 31 | 2: entity.name.function -------------------------------------------------------------------------------- /lldb_stack.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | name: AnarchyTools LLDB Stack 4 | scope: lldb.stack 5 | hidden: true 6 | 7 | contexts: 8 | main: 9 | - match: '\* Thread' 10 | scope: string 11 | push: thread 12 | 13 | - match: '\* Local variables' 14 | scope: support.class 15 | push: locals 16 | 17 | - match: '\[ continue \]' 18 | scope: keyword button btn_continue 19 | 20 | - match: '\[ pause \]' 21 | scope: keyword button btn_pause 22 | 23 | - match: '\[ step into \]' 24 | scope: keyword button btn_step_into 25 | 26 | - match: '\[ step over \]' 27 | scope: keyword button btn_step_over 28 | 29 | - match: '\[ step out \]' 30 | scope: keyword button btn_step_out 31 | 32 | - match: '\[ stop \]' 33 | scope: keyword button btn_stop 34 | 35 | thread: 36 | - meta_scope: thread 37 | - match: '([0-9]*)\s+\((.*), queue: (.*), id: ([0-9]+)\)' 38 | scope: string 39 | 40 | - match: ([0-9]+)\s+([a-zA-Z0-9._-]+)\s+([^:]+):([0-9]+)((:)([0-9]+))?\s\((.*)\) 41 | captures: 42 | 1: constant.numeric 43 | 2: entity.name.class 44 | 3: entity.name.function 45 | 4: constant.numeric 46 | 5: default 47 | 6: default 48 | 7: constant.numeric 49 | 8: entity.name.function 50 | 51 | - match: ([0-9]+)\s+([a-zA-Z0-9._-]+)\s+(.+)\s+(\+\s+[0-9]+) 52 | captures: 53 | 1: constant.numeric 54 | 2: support.class 55 | 3: comment 56 | 4: comment 57 | 58 | - match: '^(Status): (.*)' 59 | captures: 60 | 1: keyword 61 | 2: constant.other 62 | 63 | - match: '^$' 64 | pop: true 65 | 66 | locals: 67 | - meta_scope: locals 68 | 69 | - match: '(for frame )(#[0-9]+)' 70 | captures: 71 | 1: support.class 72 | 2: constant.numeric 73 | 74 | - match: (.*)( -> ) 75 | captures: 76 | 1: entity.name.variable 77 | 2: comment 78 | push: value 79 | 80 | - match: '^$' 81 | pop: true 82 | 83 | value: 84 | - match: "None" 85 | scope: keyword 86 | 87 | - match: '"' 88 | scope: string 89 | push: string 90 | 91 | - match: '$' 92 | pop: true 93 | 94 | string: 95 | - meta_scope: string 96 | - match: \\. 97 | scope: constant.character.escape 98 | - match: '"' 99 | pop: true 100 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "Readme.md" 3 | } --------------------------------------------------------------------------------