pmacs3/mode/python.py

326 lines
14 KiB
Python

import commands, os.path, sets, string, sys, traceback
import color, completer, default, mode2, method, regex, tab2
from point import Point
from lex import Grammar, PatternRule, RegionRule, OverridePatternRule
class StringGrammar(Grammar):
rules = [
PatternRule(r'octal', r'\\[0-7]{3}'),
PatternRule(r'escaped', r'\\.'),
]
class RawStringGrammar(Grammar):
rules = [
PatternRule(r'escaped', r'\\\\'),
PatternRule(r'escaped', r"\\'"),
PatternRule(r'escaped', r'\\"'),
]
class PythonGrammar(Grammar):
rules = [
PatternRule(r'functionname', r'(?<=def )[a-zA-Z_][a-zA-Z0-9_]*'),
PatternRule(r'classname', r'(?<=class )[a-zA-Z_][a-zA-Z0-9_]*'),
PatternRule(r'reserved', r'(?:True|None|False|Exception|self)(?![a-zA-Z0-9_])'),
PatternRule(r'keyword', 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(r"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(r'methodcall', r'(?<=\. )[a-zA-Z_][a-zA-Z0-9_]*(?= *\()'),
PatternRule(r'functioncall', r'[a-zA-Z_][a-zA-Z0-9_]*(?= *\()'),
PatternRule(r'system_identifier', r'__[a-zA-Z0-9_]+__'),
PatternRule(r'private_identifier', r'__[a-zA-Z0-9_]*'),
PatternRule(r'hidden_identifier', r'_[a-zA-Z0-9_]*'),
RegionRule(r'rawstring', r'r"""', RawStringGrammar, r'"""'),
RegionRule(r'rawstring', r"r'''", RawStringGrammar, r"'''"),
RegionRule(r'rawstring', r'r"', RawStringGrammar, r'"'),
RegionRule(r'rawstring', r"r'", RawStringGrammar, r"'"),
RegionRule(r'string', r'u?"""', StringGrammar, r'"""'),
RegionRule(r'string', r"u?'''", StringGrammar, r"'''"),
RegionRule(r'string', r'u?"', StringGrammar, r'"'),
RegionRule(r'string', r"u?'", StringGrammar, r"'"),
PatternRule(r'identifier', r'[a-zA-Z_][a-zA-Z0-9_]*'),
PatternRule(r'delimiter', r'\(|\)|\[|\]|{|}|@|,|:|\.|`|=|;|\+=|-=|\*=|/=|//=|%=|&=|\|=|\^=|>>=|<<=|\*\*='),
PatternRule(r"operator", r"\+|<>|<<|<=|<|-|>>|>=|>|\*\*|&|\*|\||/|\^|==|//|~|!=|%"),
PatternRule(r"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"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"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_])"),
OverridePatternRule(r'comment', r'#@@:(?P<token>[.a-zA-Z0-9_]+):(?P<mode>[.a-zA-Z0-9_]+) *$'),
PatternRule(r'comment', r'#.*$'),
PatternRule(r'continuation', r'\\\n$'),
PatternRule(r'eol', r'\n$'),
]
class PythonTabber(tab2.StackTabber):
# NOTE: yield might initially seem like an endlevel name, but it's not one.
endlevel_names = ('pass', 'return', 'raise', 'break', 'continue')
startlevel_names = ('if', 'try', 'class', 'def', 'for', 'while', 'try')
def __init__(self, m):
tab2.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)
t0 = tokens[0]
if t0.name == 'keyword' and t0.string in self.startlevel_names:
# if a line has no whitespace and beings 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 range(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 tab2.StackTabber._handle_close_token(self, currlvl, y, i)
except:
return currlvl
def _handle_other_token(self, currlvl, y, i):
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)
else:
self._opt_append('cont', currlvl + 4)
elif fqname == 'string.start':
# while inside of a string, there is no indention leve
self._opt_append('string', None)
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()
pass
elif fqname == 'keyword':
if token.string in self.endlevel_names:
# we know we'll unindent at least once
self._pop()
self.popped = True
elif token.string in self.startlevel_names and self.is_leftmost_token(y, i):
# we know we will indent exactly once
self._append(token.string, currlvl + 4)
elif token.string 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:
self._pop_until('if', 'elif')
currlvl = self.get_curr_level()
self._append(token.string, currlvl + 4)
elif token.string == '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(token.string, currlvl + 4)
elif token.string == '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(token.string, currlvl + 4)
return currlvl
class Python(mode2.Fundamental):
modename = 'Python'
extensions = ['.py']
detection = ['python']
tabbercls = PythonTabber
grammar = PythonGrammar
opentokens = ('delimiter',)
opentags = {'(': ')', '[': ']', '{': '}'}
closetokens = ('delimiter',)
closetags = {')': '(', ']': '[', '}': '{'}
colors = {
'keyword': ('cyan', 'default'),
'reserved': ('magenta', 'default'),
'builtin': ('cyan', 'default'),
'functionname': ('blue', 'default'),
'classname': ('green', 'default'),
'string.start': ('green', 'default'),
'string.null': ('green', 'default'),
'string.octal': ('magenta', 'default'),
'string.escaped': ('magenta', 'default'),
'string.end': ('green', 'default'),
'rawstring.start': ('green', 'default'),
'rawstring.null': ('green', 'default'),
'rawstring.escaped': ('green', 'default'),
'rawstring.end': ('green', 'default'),
'integer': ('default', 'default'),
'float': ('default', 'default'),
'imaginary': ('default', 'default'),
'comment': ('red', 'default'),
'continuation': ('red', 'default'),
'system_identifier': ('cyan', 'default'),
}
def __init__(self, w):
mode2.Fundamental.__init__(self, w)
# tag matching
self.add_bindings('close-paren', (')',))
self.add_bindings('close-brace', ('}',))
self.add_bindings('close-bracket', (']',))
# add python-specific methods
self.add_action_and_bindings(PythonCheckSyntax(), ('C-c s',))
self.add_action_and_bindings(PythonDictCleanup(), ('C-c h',))
# highlighting
self.pythonlib = "."
class PythonSetLib(method.Method):
'''Set the path(s) to find perl modules'''
args = [method.Argument("lib", type=type(""), prompt="Python Path: ",
default=default.build_constant("."))]
def _execute(self, w, **vargs):
w.mode.pythonlib = vargs['lib']
class PythonCheckSyntax(method.Method):
'''Check the syntax of the current python file'''
def _execute(self, w, **vargs):
sys.path.insert(0, w.mode.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)
del sys.path[0]
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)
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.kill(start_p, end_p)
w.insert_string(start_p, data)
install = Python.install