Newer
Older
invertedlogic / Environment / VimFiles / plugged / lldb.nvim / rplugin / python / lldb_nvim / session.py
from __future__ import (absolute_import, division, print_function)

from collections import OrderedDict
from time import sleep
import json

__metaclass__ = type  # pylint: disable=invalid-name


class Session:  # pylint: disable=too-many-instance-attributes

    def __init__(self, ctrl, vimx):
        import logging
        self.logger = logging.getLogger(__name__)

        self.ctrl = ctrl
        self.vimx = vimx
        self.state = OrderedDict()
        self.internal = {}
        self.json_decoder = json.JSONDecoder(object_pairs_hook=OrderedDict)
        self.help_flags = {"new": False, "launch_prompt": True, "session_show": True}
        self.bpid_map = {}

    def isalive(self):
        """ Returns true if a well-defined session is alive """
        return len(self.state) > 1 and '@file' in self.internal and '@mode' in self.internal

    def bp_map_auto(self, bp, fallback=None):
        """ Sets the value of bp.id key by trying to resolve bp to a single location;
            if not possible, sets the value to fallback
        """
        from .lldb_utils import get_bploc_tuples, settings_target_source_map
        if bp.GetNumLocations() == 1:
            self.bpid_map[bp.id] = get_bploc_tuples(
                bp, settings_target_source_map(self.ctrl.get_command_result))[0]
        else:
            self.bpid_map[bp.id] = fallback

    def new_target(self, target):
        self.bpid_map = {}
        if target.GetNumBreakpoints() > 0:
            self.logger.warn("New target has breakpoints!")
            for bp in target.breakpoint_iter():  # patch up
                self.bp_map_auto(bp)

    def bp_changed(self, cmd, bp_iter):  # pylint: disable=too-many-branches
        import re
        old_bps = set(self.bpid_map.keys())
        cur_bps = set()
        bpid_llobj_map = {}
        for bp in bp_iter:
            cur_bps.add(bp.id)
            bpid_llobj_map[bp.id] = bp

        new_bps = cur_bps - old_bps
        del_bps = old_bps - cur_bps
        if len(new_bps) == 1:
            bpid = new_bps.pop()
            bp = bpid_llobj_map[bpid]
            if re.match(r'(b|tbreak|_regexp-t?break|) \S+:[0-9]+\s*$', cmd):
                self.bp_map_auto(bp, cmd)
            else:
                self.bpid_map[bpid] = cmd
        elif len(new_bps) > 1:  # from loading a script file?
            for bpid in new_bps:
                self.bpid_map[bpid] = None
            self.logger.warn("Multiple new breakpoints!")

        if len(del_bps) > 0:
            for bpid in del_bps:
                del self.bpid_map[bpid]
            self.logger.info("Deleted breakpoints %s!", repr(list(del_bps)))

    def bp_set(self):
        if self.ctrl.target is None:
            self.vimx.log("Setting breakpoints requires a target!")
            return
        for key, vals in self.state['breakpoints'].items():
            if key == "@ll":
                for cmd in vals:
                    self.ctrl.exec_command(self.format(cmd))
            else:
                for l in vals:
                    self.ctrl.bp_set_line(key, l)

    def bp_save(self):  # pylint: disable=too-many-branches
        file_bp_map = {"@ll": []}
        for bp_val in self.bpid_map.values():
            if isinstance(bp_val, basestring):
                file_bp_map["@ll"].append(bp_val)
            elif bp_val is not None:
                abspath, line = bp_val
                relpath = self.path_shorten(abspath)
                if relpath in file_bp_map:
                    file_bp_map[relpath].append(line)
                else:
                    file_bp_map[relpath] = [line]
        for key in file_bp_map:
            if key != "@ll":
                file_bp_map[key] = sorted(set(file_bp_map[key]))
        self.state['breakpoints'] = file_bp_map

    def format(self, s):
        return s.format(**self.state['variables'])

    def run_actions(self, actions):  # pylint: disable=too-many-branches
        self.ctrl.busy_more()
        for action in actions:
            if isinstance(action, basestring):
                typ, val = 'll', action
            else:
                typ, val = action

            if typ == 'sh':
                pass  # TODO
            elif typ == 'bp':
                if val == 'set':
                    self.bp_set()
                elif val == 'save':
                    self.bp_save()
            elif typ == 'll':
                self.ctrl.exec_command(self.format(val))
        self.ctrl.busy_less()

    def get_modes(self):
        if 'modes' in self.state:
            return self.state['modes'].keys()
        else:
            return []

    def mode_setup(self, mode):
        """ Tear down the current mode, and switch to a new one. """
        if mode not in self.get_modes():
            self.vimx.log("Invalid mode!")
            return
        self.mode_teardown()
        self.internal['@mode'] = mode
        self.vimx.command("call call(g:lldb#session#mode_setup, ['%s'])" % mode)
        if 'setup' in self.state['modes'][mode]:
            self.run_actions(self.state['modes'][mode]['setup'])
        self.ctrl.update_buffers()
        if self.help_flags["new"] and \
                self.help_flags["launch_prompt"] and \
                self.internal['@mode'] == 'debug':
            sleep(0.4)
            if self.vimx.eval("input('Launch the target? [y=yes] ', 'y')") == 'y':
                self.state['modes']['debug']['setup'].append('process launch')
                self.ctrl.exec_command('process launch')
                self.vimx.log('Process launched! Try `:LLsession show`', 0)
            self.help_flags["launch_prompt"] = False

    def mode_teardown(self):
        if self.isalive():
            mode = self.internal['@mode']
            if 'teardown' in self.state['modes'][mode]:
                self.run_actions(self.state['modes'][mode]['teardown'])
            self.vimx.command("call call(g:lldb#session#mode_teardown, ['%s'])" % mode)
            del self.internal['@mode']
            return True
        return False

    def get_confpath(self):
        if self.isalive():
            from os import path
            return path.join(self.internal["@dir"], self.internal["@file"])
        else:
            return None

    def path_from_vim(self, vpath):
        from os import path
        vim_cwd = self.vimx.eval("getcwd()")
        return path.join(vim_cwd, vpath)

    def path_shorten(self, abspath):
        from os import path
        return path.relpath(abspath, self.internal["@dir"])

    def set_path(self, confpath):
        from os import path, chdir
        head, tail = path.split(path.abspath(confpath))
        if len(tail) == 0:
            self.vimx.log("Error: invalid path!")
            return False
        try:
            chdir(head)
        except OSError as e:
            self.vimx.log("%s" % e)
            return False
        self.internal["@dir"] = head
        self.internal["@file"] = tail
        return True

    def parse_and_load(self, conf_str):  # pylint: disable=too-many-branches
        # TODO if there is a nice toml encoder available, add support for it
        state = self.json_decoder.decode(conf_str)
        if not isinstance(state, dict):
            raise ValueError("The root object must be an associative array")

        for key in ["variables", "modes", "breakpoints"]:
            if key not in state:
                state[key] = {}

        for key in state:
            if key == "variables":
                pass  # TODO check validity
            elif key == "modes":
                if len(state["modes"]) == 0:
                    raise ValueError("At least one mode has to be defined")
            elif key == "breakpoints":
                pass
            else:
                raise ValueError("Invalid key '%s'" % key)

            if not isinstance(state[key], dict):
                raise ValueError('"%s" must be an associative array' % key)

        self.mode_teardown()
        self.state = state
        self.mode_setup(self.state["modes"].keys()[0])

    def handle(self, cmd, *args):  # pylint: disable=too-many-return-statements,too-many-branches
        """ Handler for :LLsession commands. """
        if cmd == 'new':
            if self.isalive() and self.vimx.eval("lldb#session#discard_prompt()") == 0:
                self.vimx.log("Session left unchanged!", 0)
                return

            ret = self.vimx.eval("lldb#session#new()")
            if not ret or '_file' not in ret:
                self.vimx.log("Skipped -- no session was created!")
                return
            if not self.set_path(ret["_file"]):
                return

            try:
                self.parse_and_load("""{
                    "variables": {},
                    "modes": {
                        "code": {},
                        "debug": {
                            "setup": [["bp", "set"]],
                            "teardown": [["bp", "save"]]
                        }
                    },
                    "breakpoints": {
                        "@ll": ["breakpoint set -n main"]
                    }
                }""")
            except ValueError as e:
                self.vimx.log("Unexpected error: " + str(e))
                return

            if 'target' in ret and len(ret['target']) > 0:
                self.state["variables"]["target"] = \
                    self.path_shorten(self.path_from_vim(ret["target"]))
                debug = self.state["modes"]["debug"]
                debug["setup"].insert(0, "target create {target}")
                debug["teardown"].append("target delete")
                self.help_flags["new"] = True

            self.vimx.log("New session created!", 0)

        elif cmd in ['load', 'reload']:
            if cmd == 'reload':
                if '@file' not in self.internal:
                    self.vimx.log("No active session!")
                    return
                if len(args) > 0:
                    self.vimx.log("Too many arguments!")
                    return
                confpath = self.get_confpath()
            elif len(args) == 0:
                confpath = self.vimx.eval('findfile(g:lldb#session#file, ".;")')
            elif len(args) == 1:
                confpath = args[0]
            else:
                self.vimx.log("Too many arguments!")
                return

            if self.isalive() and cmd == 'load' and \
                    self.vimx.eval("lldb#session#discard_prompt()") == 0:
                self.vimx.log("Session left unchanged!", 0)
                return

            try:
                with open(confpath) as f:
                    self.parse_and_load(''.join(f.readlines()))
            except (ValueError, IOError) as e:
                self.vimx.log("Bad session file: " + str(e))
            else:
                self.set_path(confpath)
                self.vimx.log("Loaded %s" % confpath, 0)

        elif cmd == 'show':
            if self.isalive():
                import re
                sfile_bufnr = self.vimx.buffer_add(self.get_confpath())
                self.vimx.command('exe "tab drop ".escape(bufname({0}), "$%# ")'
                                  .format(sfile_bufnr))

                json_str = json.dumps(self.state, indent=4, separators=(',', ': '))
                json_str = re.sub(r'\[\s*"(bp|ll|sh)",\s*"([^"]*)"\s*\]',
                                  r'[ "\1", "\2" ]', json_str)
                json_str = re.sub(r'(\[|[0-9]+,?)\s+(?=\]|[0-9]+)', r'\1 ', json_str)

                def json_show(b):
                    if b.number == sfile_bufnr:
                        b[:] = json_str.split('\n')
                        raise StopIteration

                self.vimx.map_buffers(json_show)
                if self.help_flags["new"] and self.help_flags["session_show"]:
                    self.vimx.log(
                        'Save this file, and do `:LLsession reload` to load any changes made.')
                    self.help_flags["session_show"] = False
            else:
                self.vimx.log("No active session.")

        elif cmd == 'bp-set' and self.isalive():
            self.bp_set()
        elif cmd == 'bp-save' and self.isalive():
            self.bp_save()

        else:
            self.vimx.log("Invalid sub-command: %s" % cmd)