import commands, os.path, re, string, sys, traceback
import color, completer, context, default, mode, method, regex, tab
import method.introspect
from point import Point
from render import RenderString
from lex import Grammar, PatternRule, RegionRule, OverridePatternRule
from parse import Any, And, Or, Optional, Name, Match, Matchs
from method import Method, arg, Argument
from method.shell import Exec
from etags import TagManager

class StringGrammar1(Grammar):
    rules = [
        PatternRule('octal', r'\\[0-7]{3}'),
        PatternRule('hex', r'\\x[0-9a-fA-F]{2}'),
        PatternRule('escaped', r'\\.'),
        PatternRule('data', r"[^\\']+"),
    ]
class StringGrammar2(Grammar):
    rules = [
        PatternRule('octal', r'\\[0-7]{3}'),
        PatternRule('hex', r'\\x[0-9a-fA-F]{2}'),
        PatternRule('escaped', r'\\.'),
        PatternRule('data', r'[^\\"]+'),
    ]
class StringGrammar3(Grammar):
    rules = [
        PatternRule('octal', r'\\[0-7]{3}'),
        PatternRule('hex', r'\\x[0-9a-fA-F]{2}'),
        PatternRule('escaped', r'\\.'),
        PatternRule('data', r"(?:[^\\']|'(?!')|''(?!'))+"),
    ]
class StringGrammar4(Grammar):
    rules = [
        PatternRule('octal', r'\\[0-7]{3}'),
        PatternRule('hex', r'\\x[0-9a-fA-F]{2}'),
        PatternRule('escaped', r'\\.'),
        PatternRule('data', r'(?:[^\\"]|"(?!")|""(?!"))+'),
    ]

class PythonGrammar(Grammar):
    rules = [
        PatternRule('python.def', '(?<=(?<![a-zA-Z0-9_])def )[a-zA-Z_][a-zA-Z0-9_]*'),
        PatternRule('python.class', '(?<=(?<![a-zA-Z0-9_])class )[a-zA-Z_][a-zA-Z0-9_]*'),
        PatternRule('python.reserved', '(?:True|None|False|Exception|self)(?![a-zA-Z0-9_])'),
        PatternRule('python.keyword', '(?:yield|with|while|try|return|raise|print|pass|or|not|lambda|is|in|import|if|global|from|for|finally|exec|except|else|elif|del|def|continue|class|break|assert|as|and)(?![a-zA-Z0-9_])'),
        PatternRule(r"python.builtin", r'(?<!\.)(?:zip|xrange|vars|unicode|unichr|type|tuple|super|sum|str|staticmethod|sorted|slice|setattr|set|round|repr|reduce|raw_input|range|property|pow|ord|open|oct|object|max|min|map|long|locals|list|len|iter|issubclass|isinstance|int|input|id|hex|hash|hasattr|globals|getattr|frozenset|float|filter|file|execfile|eval|enumerate|divmod|dir|dict|delattr|complex|compile|coerce|cmp|classmethod|chr|callable|bool)(?![a-zA-Z0-9_])'),
        PatternRule('python.method', r'(?<=\. )[a-zA-Z_][a-zA-Z0-9_]*(?= *\()'),
        PatternRule('python.function', r'[a-zA-Z_][a-zA-Z0-9_]*(?= *\()'),

        PatternRule('python.system_identifier', '__[a-zA-Z0-9_]+__'),
        PatternRule('python.private_identifier', '__[a-zA-Z0-9_]*'),
        PatternRule('python.hidden_identifier', '_[a-zA-Z0-9_]*'),
        PatternRule('python.identifier', '[a-zA-Z_][a-zA-Z0-9_]*(?![a-zA-Z0-9_\'"])'),

        RegionRule('rawstring', 'r"""', StringGrammar4, '"""'),
        RegionRule('rawstring', "r'''", StringGrammar3, "'''"),
        RegionRule('rawstring', 'r"', StringGrammar2, '"'),
        RegionRule('rawstring', "r'", StringGrammar1, "'"),
        RegionRule('string', 'u?"""', StringGrammar4, '"""'),
        RegionRule('string', "u?'''", StringGrammar3, "'''"),
        RegionRule('string', 'u?"', StringGrammar2, '"'),
        RegionRule('string', "u?'", StringGrammar1, "'"),

        PatternRule('delimiter', r'\(|\)|\[|\]|{|}|,|:|\.|`|=|;|\+=|-=|\*=|/=|//=|%=|&=|\|=|\^=|>>=|<<=|\*\*='),
        PatternRule(r"python.integer", r"(?<![\.0-9a-zA-Z_])(?:0|-?[1-9][0-9]*|0[0-7]+|0[xX][0-9a-fA-F]+)[lL]?(?![\.0-9a-zA-Z_])"),
        PatternRule(r"python.float", r"(?<![\.0-9a-zA-Z_])(?:-?[0-9]+\.[0-9]*|-?\.[0-9]+|(?:[0-9]|[0-9]+\.[0-9]*|-?\.[0-9]+)[eE][\+-]?[0-9]+)(?![\.0-9a-zA-Z_])"),
        PatternRule(r"python.imaginary", r"(?<![\.0-9a-zA-Z_])(?:[0-9]+|(?:[0-9]+\.[0-9]*|\.[0-9]+|(?:[0-9]|[0-9]+\.[0-9]*|\.[0-9]+)[eE][\+-]?[0-9]+)[jJ])(?![\.0-9a-zA-Z_])"),

        PatternRule(r"python.operator", r"\+|<>|<<|<=|<|-|>>|>=|>|\*\*|&|\*|\||/|\^|==|//|~|!=|%"),

        OverridePatternRule('comment', '#@@:(?P<token>[.a-zA-Z0-9_]+):(?P<mode>[.a-zA-Z0-9_]+) *$'),
        PatternRule('comment', '#.*$'),
        PatternRule('continuation', r'\\\n$'),
        PatternRule('python.decorator', '@[a-zA-Z_][a-zA-Z0-9_]*'),
        PatternRule('spaces', ' +'),
        PatternRule('eol', r'\n$'),
    ]

class PythonTabber(tab.StackTabber):
    # NOTE: yield might initially seem like an endlevel name, but it's not one.
    # NOTE: return should be an endlevel name but for now it can't be one.
    endlevel_names   = ('pass', 'raise', 'break', 'continue')
    startlevel_names = ('if', 'try', 'class', 'def', 'for', 'while', 'try')
    def __init__(self, m):
        tab.StackTabber.__init__(self, m)
        self.base_level = 0

    def is_base(self, y):
        if y == 0:
            # we always know that line 0 is indented at the 0 level
            return True
        tokens = self.get_tokens(y)
        if tokens[0].matchs('python.keyword', self.startlevel_names):
            # if a line has no whitespace and begins with something like
            # 'while','class','def','if',etc. then we can start at it
            return True
        else:
            # otherwise, we can't be sure that its level is correct
            return False

    def get_level(self, y):
        self._calc_level(y)
        return self.lines.get(y)

    def _calc_level(self, y):
        # ok, so first remember where we are going, and find our starting point
        target = y
        y = max(0, y - 1)
        while not self.is_base(y) and y > 0:
            y -= 1

        # ok, so clear out our stack and then loop over each line
        self.popped = False
        self.markers = []
        while y <= target:
            self.continued   = False
            self.last_popped = self.popped
            self.popped      = False
            tokens           = self.get_tokens(y)
            currlvl          = self.get_curr_level()
            # if we were continuing, let's pop that previous continuation token
            # and note that we're continuing
            if self.markers and self.markers[-1].name == 'cont':
                self.continued = True
                self._pop()
            # if we haven't reached the target-line yet, we can detect how many
            # levels of unindention, if any, the user chose on previous lines
            if y < target and len(tokens) > 2:
                if self.token_is_space(y, 0):
                    l = len(tokens[0].string)
                else:
                    l = 0
                while currlvl > l:
                    self._pop()
                    currlvl = self.get_curr_level()
                    self.popped = True
            # ok, having done all that, we can now process each token
            # on the line
            for i in xrange(0, len(tokens)):
                currlvl = self._handle_token(currlvl, y, i)
            # so let's store the level for this line, as well as some debugging
            self.lines[y]  = currlvl
            self.record[y] = tuple(self.markers)
            y += 1

    def _handle_close_token(self, currlvl, y, i):
        try:
            return tab.StackTabber._handle_close_token(self, currlvl, y, i)
        except:
            return currlvl

    def _handle_other_token(self, currlvl, y, i):
        w      = self.mode.tabwidth
        token  = self.get_token(y, i)
        fqname = token.fqname()
        if fqname == 'continuation':
            # we need to pop the indentation level over, unless last line was
            # also a continued line
            if self.continued:
                self._opt_append('cont', currlvl, y)
            else:
                self._opt_append('cont', currlvl + w, y)
        elif fqname == 'string.start':
            # while inside of a string, there is no indention leve
            self._opt_append('string', None, y)
        elif fqname == 'string.end':
            # since we're done with the string, resume our indentation level
            self._opt_pop('string')
        elif fqname == 'delimiter':
            # we only really care about a colon as part of a one-line statement,
            # i.e.   "while ok: foo()" or "if True: print 3"
            if token.string == ':':
                if self.markers and self.markers[-1].name in ('[', '{', '('):
                    pass
                elif self.is_rightmost_token(y, i):
                    pass
                else:
                    self._pop()
        elif fqname == 'python.keyword':
            s = token.string
            if s in self.endlevel_names and self.is_leftmost_token(y, i):
                # we know we'll unindent at least once
                self._pop()
                self.popped = True
            elif s in self.startlevel_names and self.is_leftmost_token(y, i):
                # we know we will indent exactly once
                self._append(s, currlvl + w, y)
            elif s in ('elif', 'else') and self.is_leftmost_token(y, i):
                # we know we'll unindent at least to the first if/elif
                if not self.popped and not self.last_popped and self._peek_until('if', 'elif'):
                    self._pop_until('if', 'elif')
                    currlvl = self.get_curr_level()
                self._append(s, currlvl + w, y)
            elif s == 'except' and self.is_leftmost_token(y, i):
                # we know we'll unindent at least to the first try
                if not self.popped and not self.last_popped:
                    self._pop_until('try')
                    currlvl = self.get_curr_level()
                self._append(s, currlvl + w, y)
            elif s == 'finally' and self.is_leftmost_token(y, i):
                # we know we'll unindent at least to the first try/except
                if not self.popped and not self.last_popped:
                    self._pop_until('try', 'except')
                    currlvl = self.get_curr_level()
                self._append(s, currlvl + w, y)
        return currlvl

class PythonCheckSyntax(Method):
    '''Check the syntax of the current python file'''
    def _execute(self, w, **vargs):
        pythonlib = w.application.config.get('python.lib')
        old_path = sys.path
        if pythonlib:
            sys.path.insert(0, pythonlib)
        source = w.buffer.make_string()
        try:
            code = compile(source, w.buffer.path, 'exec')
            w.set_error("Syntax OK")
        except Exception, e:
            output = traceback.format_exc()
            w.application.data_buffer("*PythonSyntax*", output,
                                      switch_to=True,
                                      modename='error')
        sys.path = old_path

class PythonDictCleanup(Method):
    '''Align assignment blocks and literal dictionaries'''
    def _execute(self, w, **vargs):
        cursor = w.logical_cursor()
        b = w.buffer

        # so this is where we will store the groups that we find
        groups_by_line = {}

        # the regex we will try
        regexes = [regex.python_dict_cleanup,
                   regex.python_assign_cleanup]

        # if we aren't in a hash, inform the user and exit
        line = b.lines[cursor.y]
        myregex = None
        for r in regexes:
            if r.match(line):
                myregex = r

        if myregex is None:
            raise Exception, "Not a python dict line"

        groups_by_line[cursor.y] = myregex.match(line).groups()
        
        # find the beginning of this hash block
        start = 0
        i = cursor.y - 1
        while i >= 0:
            line = b.lines[i]
            m = myregex.match(line)
            if not m:
                start = i + 1
                break
            else:
                groups_by_line[i] = m.groups()
            i -= 1

        # find the end of this hash block
        end = len(b.lines) - 1
        i = cursor.y + 1
        while i < len(b.lines):
            line = b.lines[i]
            m = myregex.match(line)
            if not m:
                end = i - 1
                break
            else:
                groups_by_line[i] = m.groups()
            i += 1
    
        # assume that the least indented line is correct
        indent_w = min([len(groups_by_line[k][0]) for k in groups_by_line])

        # find the longest hash key to base all the other padding on
        key_w = max([len(groups_by_line[k][1]) for k in groups_by_line])

        # for each line, format it correctly
        keys = groups_by_line.keys()
        keys.sort()
        data = ''
        for i in keys:
            indent_pad = ' ' * indent_w
            key = groups_by_line[i][1]
            sep = groups_by_line[i][3]
            value = groups_by_line[i][5]
            key_pad = ' ' * (key_w - len(key))
            if sep == '=':
                data += indent_pad + key + key_pad + ' ' + sep + ' '
            else:
                data += indent_pad + key + sep + ' ' + key_pad
            data += value + '\n'

        # remove the old text and add the new
        start_p = Point(0, start)
        if end + 1 < len(w.buffer.lines):
            end_p = Point(0, end + 1)
        else:
            end_p = Point(len(w.buffer.lines[-1]), len(w.buffer.lines) - 1)
        w.delete(start_p, end_p)
        w.insert_string(start_p, data)

class PythonHelp(Exec):
    '''Generate a help page on a python object'''
    args = [arg('name', t="string", p="Name: ", h='name to get help on')]
    def _execute(self, w, **vargs):
        name = vargs['name']
        stmt = 'try:\n  import %s\nexcept:\n  pass\nhelp(%s)' % (name, name)
        self._doit(w, None, 'python -c "%s"' % stmt)

class PythonInsertTripleSquotes(Method):
    '''Insert a triple-quoted string using single-quotes'''
    _q = "'''"
    def _execute(self, w, **vargs):
        w.insert_string_at_cursor('%s%s' % (self._q, self._q))
        for i in xrange(0, 3):
            w.backward()

class PythonInsertTripleDquotes(PythonInsertTripleSquotes):
    '''Insert a triple-quoted string using double-quotes'''
    _q = '"""'

class PythonInitNames(Method):
    '''Jump to a function defined in this module'''
    def _execute(self, w, **vargs):
        w.mode.context.build_name_map()
        w.application.set_error("Initialized name maps")

class PythonSemanticComplete(method.introspect.TokenComplete):
    _mini_prompt = 'Semantic Complete'
    def _min_completion(self, w, t):
        a = w.application
        a.methods['ipython-path-start'].execute(w, switch=False)

        #name = buffer.IperlBuffer.create_name(w.buffer)
        name = buffer.IpythonBuffer.create_name(w.buffer)
        b = a.get_buffer_by_name(name)

        line = w.buffer.lines[t.y]
        (x1, x2) = (t.x, t.end_x())
        candidates = [t.string + s for s in b.completions(line[x1:x2])]

        minlen = None
        for candidate in candidates:
            if minlen is None:
                minlen = len(candidate)
            else:
                minlen = min(minlen, len(candidate))
        
        return self._prune_candidates(t, minlen, candidates)

class PythonGotoName(Method):
    '''Jump to a class or function defined in this module'''
    args = [Argument("name", type(""), "pythonname", "Goto Name: ")]
    title = 'Name'
    def _get_dict(self, w):
        return w.mode.context.get_names()
    def _execute(self, w, **vargs):
        name = vargs['name']
        d = self._get_dict(w)
        if name in d:
            w.goto(Point(0, d[name]))
        else:
            w.application.set_error("%r %r was not found" % (title, name))

class PythonGotoFunction(PythonGotoName):
    '''Jump to a function defined in this module'''
    args = [Argument("name", type(""), "pythonfunction", "Goto Function: ")]
    title = 'Function'
    def _get_dict(self, w):
        return w.mode.context.get_functions()

class PythonGotoClass(Method):
    '''Jump to a class defined in this module'''
    args = [Argument("name", type(""), "pythonclass", "Goto Class: ")]
    title = 'Class'
    def _get_dict(self, w):
        return w.mode.context.get_classes()

class PythonListNames(Method):
    '''Show the user all functions defined in this module'''
    def _execute(self, w, **vargs):
        names = w.mode.context.get_names()
        output = '\n'.join(sorted(names)) + "\n"
        w.application.data_buffer("*Python-List-Names*", output, switch_to=True)

class PythonBrmFindReferences(Method):
    def _execute(self, w, **vargs):
        if w.mode.brm is None:
            w.set_error('bicycle repairman not installed')
            return

        base = os.getcwd()
        path = w.buffer.path
        cursor = w.logical_cursor()
        line, col  = cursor.y + 1, cursor.x + 1
        refs = w.mode.brm.findReferencesByCoordinates(path, line, col)

        l, count, tokens = 0, 0, []
        if not base.endswith('/'): base += '/'

        for r in refs:
            f, n, c = r.filename, r.lineno, r.confidence
            f = f.replace(base, '')
            label = '%s:%d:' % (f, n)
            l = max(len(label), l)
            tokens.append((label, c))
            count += 1

        lines = []
        for tpl in tokens:
            lines.append('%-*s        %3d%% confidence' % (l, tpl[0], tpl[1]))
        
        if not tokens:
            w.set_error('no references found')
            return
            
        data = '\n'.join(lines)
        w.application.data_buffer("*References*", data, switch_to=True)
        if count == 1:
            w.set_error('1 reference found')
        else:
            w.set_error('%d references found' % count)

class PythonNameCompleter(completer.Completer):
    def _get_dict(self, w):
        return w.buffer.method.old_window.mode.context.get_names()
    def get_candidates(self, s, w=None):
        return [n for n in self._get_dict(w) if n.startswith(s)]
class PythonFunctionCompleter(PythonNameCompleter):
    def _get_dict(self, w):
        return w.buffer.method.old_window.mode.context.get_functions()
class PythonClassCompleter(completer.Completer):
    def _get_dict(self, w):
        return w.buffer.method.old_window.mode.context.get_classes()

class PythonContext(context.Context):
    empty_match = And(Optional(Name('spaces')), Name('eol'))
    class_match = And(Optional(Name('spaces')),
                      Match('python.keyword', 'class'),
                      Name('spaces'),
                      Name('python.class'))
    func_match = And(Optional(Name('spaces')),
                     Match('python.keyword', 'def'),
                     Name('spaces'),
                     Name('python.def'))
    def __init__(self, mode):
        self.mode      = mode
        self.names     = None
        self.namelines = None
        self.classes   = None
        self.functions = None

    # new object methods
    def get_functions(self):
        if self.functions is None:
            self.build_name_map()
        return self.functions
    def get_classes(self):
        if self.classes is None:
            self.build_name_map()
        return self.classes
    def get_function_list(self):
        return self._ordered_dict(self.get_functions())
    def get_class_list(self):
        return self._ordered_dict(self.get_classes())

    # overridden object methods
    def _init_name_map(self):
        self.names     = {}
        self.classes   = {}
        self.functions = {}
        self.namelines = [(None, None)] * len(self.mode.window.buffer.lines)
    def _del_name(self, y, name):
        if name:
            if name in self.names:
                del self.names[name]
            if name in self.classes:
                del self.classes[name]
            if name in self.functions:
                del self.functions[name]
        self.namelines[y] = (None, None)
    def _build_name_map(self, y1, y2, last, curr, stack):
        blen       = len(self.mode.window.buffer.lines)
        highlights = self.mode.window.get_highlighter()
        i          = y1
        abbrev     = {}
        while i < y2:
            tokens = highlights.tokens[i]
            g = highlights.tokens[i]
            if self.empty_match.match(tokens):
                if last is None:
                    last = i
                i += 1
                continue

            if g[0].name == 'spaces':
                j, lvl = 1, len(g[0].string)
            else:
                j, lvl = 0, 0

            while stack and lvl <= stack[-1][0]:
                stack.pop(-1)

            if last is not None:
                curr = '.'.join([x[1] for x in stack])
                if curr:
                    for k in xrange(last, i):
                        self.namelines[k] = (curr, None)
                last = None

            if len(g[j:]) > 3:
                found = False
                if g[j].name == 'python.keyword' and g[j].string == 'class':
                    found = True
                elif g[j].name == 'python.keyword' and g[j].string == 'def':
                    found = True
                if found:
                    stack.append([lvl, g[j+2].string])
                    curr = '.'.join([x[1] for x in stack])
                    self.names[curr] = i

                    for k in xrange(1, len(stack)):
                        curr = '.'.join(x[1] for x in stack[k:])
                        if curr not in abbrev:
                            abbrev[curr] = i
                        else:
                            abbrev[curr] = None
            else:
                curr = '.'.join([x[1] for x in stack])

            if i == y2 - 1 and curr != self.namelines[i][0] and y2 < blen:
                y2 += 1
            if curr:
                self.namelines[i] = (curr, None)
            i += 1

        for name in abbrev:
            if abbrev[name] is not None:
                self.names[name] = abbrev[name]

        if last is not None and y2 < len(self.namelines):
            if self.namelines[y2] and self.namelines[y2][0]:
                n = len(self.namelines[y2][0].split('.'))
                curr = '.'.join([x[1] for x in stack[:n]])
            if curr:
                for k in xrange(last, y2):
                    self.namelines[k] = (curr, None)

class PythonTagManager(TagManager):
    lang  = 'Python'
    exts  = set(('.py',))

# white is for delimiters, operators, numbers
default = ('default', 'default')

# magenta is for reserved words
lo_magenta = ('magenta202', 'default')
hi_magenta = ('magenta505', 'default')

# red is for comments
lo_red = ('red300', 'default')
hi_red = ('red511', 'default')

# orange is unused
hi_orange = ('yellow531', 'default')
lo_orange = ('yellow520', 'default')

# yellow is for class names
hi_yellow = ('yellow551', 'default')
lo_yellow = ('yellow330', 'default')

# green is for strings
lo_green = ('green030', 'default')
hi_green = ('green050', 'default')

# cyan is for keywords and some operators
lo_cyan = ('cyan033', 'default')
hi_cyan = ('cyan155', 'default')

# blue is for functions and methods
lo_blue = ('blue113', 'default')
hi_blue = ('blue225', 'default')

class Python(mode.Fundamental):
    description = '''
    This programming mode is designed to edit Python source files. It
    features parenthesis matching, syntax highlighting, indentation
    assistance and syntax checking. It can also find classes and functions
    by name, provide scope context in the status bar, and has an optional
    context header. Finally, it can semantically complete tokens and
    provide help output via the python interpreter.
'''
    name        = 'Python'
    extensions  = ['.py']
    detection   = [re.compile('^#!(?:.+[/ ])python')]
    tabbercls   = PythonTabber
    tagcls      = PythonTagManager
    grammar     = PythonGrammar
    opentokens  = ('delimiter',)
    opentags    = {'(': ')', '[': ']', '{': '}'}
    closetokens = ('delimiter',)
    closetags   = {')': '(', ']': '[', '}': '{'}
    commentc    = '#'
    colors      = {
        'python.def':                hi_blue,
        'python.class':              hi_yellow,
        'python.decorator':          lo_magenta,
        'python.reserved':           hi_magenta,
        'python.keyword':            hi_cyan,
        'python.builtin':            hi_cyan,
        'python.method':             default,
        'python.function':           default,
        'python.system_identifier':  hi_cyan,
        'python.private_identifier': default,
        'python.hidden_identifier':  default,
        'python.identifier':         default,
        'python.integer':            default,
        'python.float':              default,
        'python.imaginary':          default,
        'python.operator':           default,
        'rawstring.start':           lo_green,
        'rawstring.end':             lo_green,
        'rawstring.data':            hi_green,
        'rawstring.null':            hi_green,
        'rawstring.escaped':         hi_magenta,
    }
    config = {
        'python.lib': '.',
    }
    lconfig = {
        'ignore_suffix': ['.pyc', '.pyo'],
    }
    actions = [PythonInitNames, PythonListNames, PythonGotoName, PythonHelp,
               PythonGotoFunction, PythonGotoClass, PythonCheckSyntax,
               PythonDictCleanup, PythonSemanticComplete,
               PythonBrmFindReferences,
               PythonInsertTripleSquotes, PythonInsertTripleDquotes]
    completers = {
        "pythonname":     PythonNameCompleter(None),
        "pythonfunction": PythonFunctionCompleter(None),
        "pythonclass":    PythonClassCompleter(None),
    }

    format = "%(flag)s  %(bname)s  (%(mname)s)  %(indent)s  %(cursor)s  %(perc)s  [%(name)s]  %(vc-info)s"
    header_size = 3

    def get_status_names(self):
        names = mode.Fundamental.get_status_names(self)
        c = self.window.logical_cursor()
        names['name'] = self.context.get_line_name(c.y)
        return names

    # xyz
    def get_header(self):
        fg, bg = "default", "blue"

        if self.tabber is None:
            s = "Header support is not available for this mode"
            hs = [[RenderString(s=s, attrs=color.build(fg, bg))]]
            while len(hs) < 3:
                hs.insert(0, [RenderString(s='', attrs=color.build(fg, bg))])
            return hs

        w = self.window
        y = self.window.first.y
        if self.window.first.x > 0:
            y += 1
        lvl = self.tabber.get_level(y)
        markers = self.tabber.record[y]
        if w.buffer.is_whitespace(y):
            ws = None
        else:
            ws = w.buffer.count_leading_whitespace(y)

        hs = []
        i = len(markers) - 1
        while i >= 0 and len(hs) < 3:
            marker = markers[i]
            i -= 1
            if marker.y == y:
                continue
            if ws and marker.level > ws:
                continue
            s = w.buffer.lines[marker.y][:w.width - 1]
            hs.insert(0, [RenderString(s=s, attrs=color.build(fg, bg))])
        while len(hs) < 3:
            hs.insert(0, [RenderString(s='', attrs=color.build(fg, bg))])
        return hs

    def __init__(self, w):
        mode.Fundamental.__init__(self, w)
        self.add_bindings('close-paren', (')',))
        self.add_bindings('close-brace', ('}',))
        self.add_bindings('close-bracket', (']',))
        self.add_bindings('python-goto-name', ('C-c M-g',))
        self.add_bindings('python-goto-function', ('C-c M-f',))
        self.add_bindings('python-goto-class', ('C-c M-c',))
        self.add_bindings('python-check-syntax', ('C-c s',))
        self.add_bindings('python-dict-cleanup', ('C-c h',))
        self.add_bindings('python-insert-triple-squotes', ('C-c M-\'',))
        self.add_bindings('python-insert-triple-dquotes', ('C-c M-"',))
        self.add_bindings('python-semantic-complete', ('C-c TAB',))
        self.context = PythonContext(self)

        # bicycle repairman!
        try:
            import bike
            self.brm = bike.init()
            # turn off brm's annoying STDERR printing
            f = open('/dev/null', 'w')
            self.brm.setProgressLogger(f)
            self.brm.setWarningLogger(f)
        except ImportError:
            self.brm = None

install = Python.install