import commands, os.path, sets, string, sys import color, completer, default, mode2, lex2, method, regex, tab2 import ctag_python from point2 import Point from lex2 import Grammar, ConstantRule, PatternRule, RegionRule, DualRegionRule class StringGrammar(Grammar): rules = [ PatternRule( name=r'octal', pattern=r'\\[0-7]{3}', ), PatternRule( name=r'escaped', pattern=r'\\.', ), #PatternRule( # name=r'format', # pattern=r'%(?:\([a-zA-Z_]+\))?[-# +]*(?:[0-9]+|\*)?\.?(?:[0-9]+|\*)?[hlL]?[a-zA-Z%]', #), ] class PythonGrammar(Grammar): rules = [ PatternRule( name=r'functiondef', pattern=r'(?<=def )[a-zA-Z_][a-zA-Z0-9_]*', ), PatternRule( name=r'classdef', pattern=r'(?<=class )[a-zA-Z_][a-zA-Z0-9_]*', ), PatternRule( name=r'reserved', pattern=r'(?:True|None|False|Exception|self)(?![a-zA-Z0-9_])', ), PatternRule( name=r'keyword', pattern=r'(?:yield|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( name=r"builtin", pattern=r'(?>=|<<=|\*\*=', ), PatternRule( name=r"operator", pattern=r"\+|<>|<<|<=|<|-|>>|>=|>|\*\*|&|\*|\||/|\^|==|//|~|!=|%", ), PatternRule( name=r"integer", pattern=r"(?"""|\'\'\')', grammar=Grammar(), end=r'%(tag)s', ), RegionRule( name=r'tq_string', start=r'(?P"""|\'\'\')', grammar=Grammar(), end=r'%(tag)s', ), RegionRule( name=r'string', start=r'(?P"|\')', grammar=StringGrammar(), end=r'%(tag)s', ), PatternRule( name=r'comment', pattern=r'#.*$', ), PatternRule( name=r'continuation', pattern=r'\\$', ), ] class PythonTabber(tab2.Tabber): def is_base(self, y): if y == 0: return True highlighter = self.mode.window.buffer.highlights[self.mode.name()] if not highlighter.tokens[y]: return False t = highlighter.tokens[y][0] return t.name == 'keyword' and t.string == 'def': def __init__(self, m): tab2.Tabber.__init__(self, m) def stack_append(self, item): self.tab_stack.append(item) def stack_pop(self): self.tab_stack.pop(-1) def base_indentation_level(self, y): return y == 0 def calculate_tabs(self, start=0, goal=None): lines = self.mode.window.buffer.lines tokens = self.mode.highlighter.tokens buffer = self.mode.window.buffer if self.levels is None: self.levels = [None] * (len(lines)) self.index = 0 self.y = start self.base = 0 self.tab_stack = [] # we want to process every logical line in the file while self.y < len(lines): line = lines[self.y] start_index = self.index start_point = point.Point(0, self.y) start_offset = buffer.get_point_offset(start_point) end_point = point.Point(len(line), self.y) end_offset = buffer.get_point_offset(end_point) # we want to find all the tokens on the line we are currently processing while self.index < len(tokens): token = tokens[self.index] if token.end > end_offset: break self.index += 1 self.handle_line(line, start_offset, start_index, end_offset, self.index) self.levels[self.y] = self.line_depth self.y += 1 if goal is not None and self.y > goal: break def get_line_depth(self): if len(self.tab_stack) > 0: return self.tab_stack[-1][1] else: return self.base def handle_line(self, line, start_offset, start_index, end_offset, end_index): self.line_depth = self.get_line_depth() tokens = self.mode.highlighter.tokens if start_index >= len(tokens): return if regex.whitespace.match(line): return if len(self.tab_stack) == 0 and tokens[start_index].start >= start_offset: self.base = util.count_leading_whitespace(line) for i in range(start_index, end_index): token = tokens[i] s = token.string if s in self.start_tags: if i < end_index - 1: i = tokens[i+1].start - start_offset elif len(self.tab_stack) > 0: i = self.tab_stack[-1][1] + 4 else: i = self.base + 4 self.stack_append((s, i)) elif s in self.close_tags: assert len(self.tab_stack), "Unbalanced closing tag" assert self.tab_stack[-1][0] == self.close_tags[s], "Unmatched closing tag" self.stack_pop() if i == start_index: self.line_depth = self.get_line_depth() if tokens[start_index].start < start_offset: self.line_depth = -1 prebase = self.base s = tokens[start_index].string e = tokens[end_index-1].string if s == "except" or s == "elif" or s == "else": if self.y > 0 and self.line_depth == self.levels[self.y - 1]: self.line_depth = max(0, self.line_depth - 4) elif (s == "return" or s == "raise" or s == "yield" or s == "break" or s == "pass" or s == 'continue'): self.base = max(0, self.base - 4) if e == "\\": if len(self.tab_stack) and self.tab_stack[-1][0] == "\\": pass else: self.stack_append(("\\", prebase + 4)) return elif e == ":": self.base += 4 elif len(self.tab_stack) and self.tab_stack[-1][0] == "\\": self.stack_pop() def get_indentation_level(self, y): if self.levels is not None and self.levels[y] is not None: result = self.levels[y] else: i = max(0, y - 1) while i > 0: if self.base_indentation_level(i): break i -= 1 self.calculate_tabs(i, y) result = self.levels[y] if result == -1: return None return result class Python(mode2.Fundamental): tabbercls = PythonTabber grammar = PythonGrammar() opentoken = 'delimiter' opentags = {'(': ')', '[': ']', '{': '}'} closetoken = 'delimiter' closetags = {')': '(', ']': '[', '}': '{'} def __init__(self, w): mode2.Fundamental.__init__(self, w) # add python-specific methods self.add_action_and_bindings(PythonCheckSyntax(), ('C-c s',)) self.add_action_and_bindings(PythonDictCleanup(), ('C-c h',)) self.add_action_and_bindings(PythonUpdateTags(), ('C-c t',)) self.add_action_and_bindings(PythonTagComplete(), ('C-c k',)) # we want to do these kinds of tag matching self.add_bindings('close-paren', (')',)) self.add_bindings('close-brace', ('}',)) self.add_bindings('close-bracket', (']',)) self.colors = { 'keyword': color.build('cyan', 'default'), 'reserved': color.build('cyan', 'default'), 'builtin': color.build('cyan', 'default'), 'functiondef': color.build('blue', 'default'), 'classdef': color.build('green', 'default'), 'string.start': color.build('green', 'default'), 'string.null': color.build('green', 'default'), 'string.escaped': color.build('magenta', 'default'), 'string.octal': color.build('magenta', 'default'), 'string.format': color.build('yellow', 'default'), 'string.end': color.build('green', 'default'), 'integer': color.build('default', 'default'), 'float': color.build('default', 'default'), 'imaginary': color.build('default', 'default'), 'tq_string.start': color.build('green', 'default'), 'tq_string.null': color.build('green', 'default'), 'tq_string.end': color.build('green', 'default'), 'docstring.start': color.build('green', 'default'), 'docstring.null': color.build('green', 'default'), 'docstring.end': color.build('green', 'default'), 'comment': color.build('red', 'default'), 'continuation': color.build('red', 'default'), 'system_identifier': color.build('cyan', 'default'), } def name(self): return "Python" class PythonCheckSyntax(method.Method): '''Check the syntax of the current python file''' def _args(self): return [method.Argument("lib", type=type(""), prompt="Python Path: ", datatype='path', default=default.build_constant("."))] def _execute(self, w, **vargs): a = vargs['lib'] mod = os.path.splitext(os.path.basename(w.buffer.path))[0] cmd = "PYTHONPATH=%s python -c 'import %s'" % (a, mod) (status, output) = commands.getstatusoutput(cmd) if status == 0: w.application.set_error("Syntax OK") w.application.data_buffer("python-syntax", output, switch_to=False) else: output = output + "\ncommand exit status: %d" % (status) w.application.data_buffer("python-syntax", output, switch_to=True) class PythonUpdateTags(method.Method): '''Update the CTag data associated with a python buffer''' def _args(self): return [method.Argument("lib", prompt="Module Base: ", datatype='path', default=default.build_constant("."))] def _execute(self, w, **vargs): w.mode.ctagger = ctag_python.PythonCTagger() w.mode.ctagger.process_paths([vargs['lib']]) w.application.set_error('Tag data updated') class PythonTagComplete(method.Method): '''Complete a symbol using tag data''' def _execute(self, w, **vargs): if not w.mode.ctagger.packages: w.application.methods['python-update-tags'].execute(w) return cursor = w.logical_cursor() b = w.buffer line = b.lines[cursor.y] end = cursor.x start = cursor.x word_chars = string.letters + string.digits + '_' if start == 0: w.application.set_error('walrus 1') return c = line[start - 1] if c == '(': w.application.set_error('goldfinch 1') return elif c not in word_chars: w.application.set_error('walrus 2') return while start > 0 and line[start - 1] in word_chars: start -= 1 if start == end: w.application.set_error('walrus 3') return word = line[start:end] candidates = [] seen = sets.Set() for p in w.mode.ctagger.packages.iterkeys(): if p.startswith(word): if p in seen: continue candidates.append(p) seen.add(p) for e in w.mode.ctagger.entries.itervalues(): if e.symbol.startswith(word): if e.symbol in seen: continue candidates.append(e.symbol) seen.add(e.symbol) if len(candidates) == 0: w.application.set_error('No match: %r' % word) return elif len(candidates) == 1: newword = candidates[0] if word == newword: w.application.set_error('Already completed!') return else: w.application.set_error('Unique match!') else: newword = completer.find_common_string(candidates) w.application.set_error('Ambiguous match: %r' % (candidates)) b.delete_string(Point(start, cursor.y), Point(end, cursor.y)) b.insert_string(Point(start, cursor.y), newword) class PythonDictCleanup(method.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 + ' ' + value + '\n' else: data += indent_pad + key + sep + ' ' + key_pad + value + '\n' # remove the old text and add the new start_p = Point(0, start) end_p = Point(0, end + 1) w.kill(start_p, end_p) w.insert(start_p, data)