530 lines
21 KiB
Python
530 lines
21 KiB
Python
|
import sys
|
||
|
import string, tokenize
|
||
|
from . import PyParse
|
||
|
from pywin import default_scintilla_encoding
|
||
|
|
||
|
if sys.version_info < (3,):
|
||
|
# in py2k, tokenize() takes a 'token eater' callback, while
|
||
|
# generate_tokens is a generator that works with str objects.
|
||
|
token_generator = tokenize.generate_tokens
|
||
|
else:
|
||
|
# in py3k tokenize() is the generator working with 'byte' objects, and
|
||
|
# token_generator is the 'undocumented b/w compat' function that
|
||
|
# theoretically works with str objects - but actually seems to fail)
|
||
|
token_generator = tokenize.tokenize
|
||
|
|
||
|
class AutoIndent:
|
||
|
|
||
|
menudefs = [
|
||
|
('edit', [
|
||
|
None,
|
||
|
('_Indent region', '<<indent-region>>'),
|
||
|
('_Dedent region', '<<dedent-region>>'),
|
||
|
('Comment _out region', '<<comment-region>>'),
|
||
|
('U_ncomment region', '<<uncomment-region>>'),
|
||
|
('Tabify region', '<<tabify-region>>'),
|
||
|
('Untabify region', '<<untabify-region>>'),
|
||
|
('Toggle tabs', '<<toggle-tabs>>'),
|
||
|
('New indent width', '<<change-indentwidth>>'),
|
||
|
]),
|
||
|
]
|
||
|
|
||
|
keydefs = {
|
||
|
'<<smart-backspace>>': ['<Key-BackSpace>'],
|
||
|
'<<newline-and-indent>>': ['<Key-Return>', '<KP_Enter>'],
|
||
|
'<<smart-indent>>': ['<Key-Tab>']
|
||
|
}
|
||
|
|
||
|
windows_keydefs = {
|
||
|
'<<indent-region>>': ['<Control-bracketright>'],
|
||
|
'<<dedent-region>>': ['<Control-bracketleft>'],
|
||
|
'<<comment-region>>': ['<Alt-Key-3>'],
|
||
|
'<<uncomment-region>>': ['<Alt-Key-4>'],
|
||
|
'<<tabify-region>>': ['<Alt-Key-5>'],
|
||
|
'<<untabify-region>>': ['<Alt-Key-6>'],
|
||
|
'<<toggle-tabs>>': ['<Alt-Key-t>'],
|
||
|
'<<change-indentwidth>>': ['<Alt-Key-u>'],
|
||
|
}
|
||
|
|
||
|
unix_keydefs = {
|
||
|
'<<indent-region>>': ['<Alt-bracketright>',
|
||
|
'<Meta-bracketright>',
|
||
|
'<Control-bracketright>'],
|
||
|
'<<dedent-region>>': ['<Alt-bracketleft>',
|
||
|
'<Meta-bracketleft>',
|
||
|
'<Control-bracketleft>'],
|
||
|
'<<comment-region>>': ['<Alt-Key-3>', '<Meta-Key-3>'],
|
||
|
'<<uncomment-region>>': ['<Alt-Key-4>', '<Meta-Key-4>'],
|
||
|
'<<tabify-region>>': ['<Alt-Key-5>', '<Meta-Key-5>'],
|
||
|
'<<untabify-region>>': ['<Alt-Key-6>', '<Meta-Key-6>'],
|
||
|
'<<toggle-tabs>>': ['<Alt-Key-t>'],
|
||
|
'<<change-indentwidth>>': ['<Alt-Key-u>'],
|
||
|
}
|
||
|
|
||
|
# usetabs true -> literal tab characters are used by indent and
|
||
|
# dedent cmds, possibly mixed with spaces if
|
||
|
# indentwidth is not a multiple of tabwidth
|
||
|
# false -> tab characters are converted to spaces by indent
|
||
|
# and dedent cmds, and ditto TAB keystrokes
|
||
|
# indentwidth is the number of characters per logical indent level.
|
||
|
# tabwidth is the display width of a literal tab character.
|
||
|
# CAUTION: telling Tk to use anything other than its default
|
||
|
# tab setting causes it to use an entirely different tabbing algorithm,
|
||
|
# treating tab stops as fixed distances from the left margin.
|
||
|
# Nobody expects this, so for now tabwidth should never be changed.
|
||
|
usetabs = 1
|
||
|
indentwidth = 4
|
||
|
tabwidth = 8 # for IDLE use, must remain 8 until Tk is fixed
|
||
|
|
||
|
# If context_use_ps1 is true, parsing searches back for a ps1 line;
|
||
|
# else searches for a popular (if, def, ...) Python stmt.
|
||
|
context_use_ps1 = 0
|
||
|
|
||
|
# When searching backwards for a reliable place to begin parsing,
|
||
|
# first start num_context_lines[0] lines back, then
|
||
|
# num_context_lines[1] lines back if that didn't work, and so on.
|
||
|
# The last value should be huge (larger than the # of lines in a
|
||
|
# conceivable file).
|
||
|
# Making the initial values larger slows things down more often.
|
||
|
num_context_lines = 50, 500, 5000000
|
||
|
|
||
|
def __init__(self, editwin):
|
||
|
self.editwin = editwin
|
||
|
self.text = editwin.text
|
||
|
|
||
|
def config(self, **options):
|
||
|
for key, value in options.items():
|
||
|
if key == 'usetabs':
|
||
|
self.usetabs = value
|
||
|
elif key == 'indentwidth':
|
||
|
self.indentwidth = value
|
||
|
elif key == 'tabwidth':
|
||
|
self.tabwidth = value
|
||
|
elif key == 'context_use_ps1':
|
||
|
self.context_use_ps1 = value
|
||
|
else:
|
||
|
raise KeyError("bad option name: %s" % repr(key))
|
||
|
|
||
|
# If ispythonsource and guess are true, guess a good value for
|
||
|
# indentwidth based on file content (if possible), and if
|
||
|
# indentwidth != tabwidth set usetabs false.
|
||
|
# In any case, adjust the Text widget's view of what a tab
|
||
|
# character means.
|
||
|
|
||
|
def set_indentation_params(self, ispythonsource, guess=1):
|
||
|
if guess and ispythonsource:
|
||
|
i = self.guess_indent()
|
||
|
if 2 <= i <= 8:
|
||
|
self.indentwidth = i
|
||
|
if self.indentwidth != self.tabwidth:
|
||
|
self.usetabs = 0
|
||
|
|
||
|
self.editwin.set_tabwidth(self.tabwidth)
|
||
|
|
||
|
def smart_backspace_event(self, event):
|
||
|
text = self.text
|
||
|
first, last = self.editwin.get_selection_indices()
|
||
|
if first and last:
|
||
|
text.delete(first, last)
|
||
|
text.mark_set("insert", first)
|
||
|
return "break"
|
||
|
# Delete whitespace left, until hitting a real char or closest
|
||
|
# preceding virtual tab stop.
|
||
|
chars = text.get("insert linestart", "insert")
|
||
|
if chars == '':
|
||
|
if text.compare("insert", ">", "1.0"):
|
||
|
# easy: delete preceding newline
|
||
|
text.delete("insert-1c")
|
||
|
else:
|
||
|
text.bell() # at start of buffer
|
||
|
return "break"
|
||
|
if chars[-1] not in " \t":
|
||
|
# easy: delete preceding real char
|
||
|
text.delete("insert-1c")
|
||
|
return "break"
|
||
|
# Ick. It may require *inserting* spaces if we back up over a
|
||
|
# tab character! This is written to be clear, not fast.
|
||
|
have = len(chars.expandtabs(self.tabwidth))
|
||
|
assert have > 0
|
||
|
want = int((have - 1) / self.indentwidth) * self.indentwidth
|
||
|
ncharsdeleted = 0
|
||
|
while 1:
|
||
|
chars = chars[:-1]
|
||
|
ncharsdeleted = ncharsdeleted + 1
|
||
|
have = len(chars.expandtabs(self.tabwidth))
|
||
|
if have <= want or chars[-1] not in " \t":
|
||
|
break
|
||
|
text.undo_block_start()
|
||
|
text.delete("insert-%dc" % ncharsdeleted, "insert")
|
||
|
if have < want:
|
||
|
text.insert("insert", ' ' * (want - have))
|
||
|
text.undo_block_stop()
|
||
|
return "break"
|
||
|
|
||
|
def smart_indent_event(self, event):
|
||
|
# if intraline selection:
|
||
|
# delete it
|
||
|
# elif multiline selection:
|
||
|
# do indent-region & return
|
||
|
# indent one level
|
||
|
text = self.text
|
||
|
first, last = self.editwin.get_selection_indices()
|
||
|
text.undo_block_start()
|
||
|
try:
|
||
|
if first and last:
|
||
|
if index2line(first) != index2line(last):
|
||
|
return self.indent_region_event(event)
|
||
|
text.delete(first, last)
|
||
|
text.mark_set("insert", first)
|
||
|
prefix = text.get("insert linestart", "insert")
|
||
|
raw, effective = classifyws(prefix, self.tabwidth)
|
||
|
if raw == len(prefix):
|
||
|
# only whitespace to the left
|
||
|
self.reindent_to(effective + self.indentwidth)
|
||
|
else:
|
||
|
if self.usetabs:
|
||
|
pad = '\t'
|
||
|
else:
|
||
|
effective = len(prefix.expandtabs(self.tabwidth))
|
||
|
n = self.indentwidth
|
||
|
pad = ' ' * (n - effective % n)
|
||
|
text.insert("insert", pad)
|
||
|
text.see("insert")
|
||
|
return "break"
|
||
|
finally:
|
||
|
text.undo_block_stop()
|
||
|
|
||
|
def newline_and_indent_event(self, event):
|
||
|
text = self.text
|
||
|
first, last = self.editwin.get_selection_indices()
|
||
|
text.undo_block_start()
|
||
|
try:
|
||
|
if first and last:
|
||
|
text.delete(first, last)
|
||
|
text.mark_set("insert", first)
|
||
|
line = text.get("insert linestart", "insert")
|
||
|
i, n = 0, len(line)
|
||
|
while i < n and line[i] in " \t":
|
||
|
i = i+1
|
||
|
if i == n:
|
||
|
# the cursor is in or at leading indentation; just inject
|
||
|
# an empty line at the start and strip space from current line
|
||
|
text.delete("insert - %d chars" % i, "insert")
|
||
|
text.insert("insert linestart", '\n')
|
||
|
return "break"
|
||
|
indent = line[:i]
|
||
|
# strip whitespace before insert point
|
||
|
i = 0
|
||
|
while line and line[-1] in " \t":
|
||
|
line = line[:-1]
|
||
|
i = i+1
|
||
|
if i:
|
||
|
text.delete("insert - %d chars" % i, "insert")
|
||
|
# strip whitespace after insert point
|
||
|
while text.get("insert") in " \t":
|
||
|
text.delete("insert")
|
||
|
# start new line
|
||
|
text.insert("insert", '\n')
|
||
|
|
||
|
# adjust indentation for continuations and block
|
||
|
# open/close first need to find the last stmt
|
||
|
lno = index2line(text.index('insert'))
|
||
|
y = PyParse.Parser(self.indentwidth, self.tabwidth)
|
||
|
for context in self.num_context_lines:
|
||
|
startat = max(lno - context, 1)
|
||
|
startatindex = repr(startat) + ".0"
|
||
|
rawtext = text.get(startatindex, "insert")
|
||
|
y.set_str(rawtext)
|
||
|
bod = y.find_good_parse_start(
|
||
|
self.context_use_ps1,
|
||
|
self._build_char_in_string_func(startatindex))
|
||
|
if bod is not None or startat == 1:
|
||
|
break
|
||
|
y.set_lo(bod or 0)
|
||
|
c = y.get_continuation_type()
|
||
|
if c != PyParse.C_NONE:
|
||
|
# The current stmt hasn't ended yet.
|
||
|
if c == PyParse.C_STRING:
|
||
|
# inside a string; just mimic the current indent
|
||
|
text.insert("insert", indent)
|
||
|
elif c == PyParse.C_BRACKET:
|
||
|
# line up with the first (if any) element of the
|
||
|
# last open bracket structure; else indent one
|
||
|
# level beyond the indent of the line with the
|
||
|
# last open bracket
|
||
|
self.reindent_to(y.compute_bracket_indent())
|
||
|
elif c == PyParse.C_BACKSLASH:
|
||
|
# if more than one line in this stmt already, just
|
||
|
# mimic the current indent; else if initial line
|
||
|
# has a start on an assignment stmt, indent to
|
||
|
# beyond leftmost =; else to beyond first chunk of
|
||
|
# non-whitespace on initial line
|
||
|
if y.get_num_lines_in_stmt() > 1:
|
||
|
text.insert("insert", indent)
|
||
|
else:
|
||
|
self.reindent_to(y.compute_backslash_indent())
|
||
|
else:
|
||
|
assert 0, "bogus continuation type " + repr(c)
|
||
|
return "break"
|
||
|
|
||
|
# This line starts a brand new stmt; indent relative to
|
||
|
# indentation of initial line of closest preceding
|
||
|
# interesting stmt.
|
||
|
indent = y.get_base_indent_string()
|
||
|
text.insert("insert", indent)
|
||
|
if y.is_block_opener():
|
||
|
self.smart_indent_event(event)
|
||
|
elif indent and y.is_block_closer():
|
||
|
self.smart_backspace_event(event)
|
||
|
return "break"
|
||
|
finally:
|
||
|
text.see("insert")
|
||
|
text.undo_block_stop()
|
||
|
|
||
|
auto_indent = newline_and_indent_event
|
||
|
|
||
|
# Our editwin provides a is_char_in_string function that works
|
||
|
# with a Tk text index, but PyParse only knows about offsets into
|
||
|
# a string. This builds a function for PyParse that accepts an
|
||
|
# offset.
|
||
|
|
||
|
def _build_char_in_string_func(self, startindex):
|
||
|
def inner(offset, _startindex=startindex,
|
||
|
_icis=self.editwin.is_char_in_string):
|
||
|
return _icis(_startindex + "+%dc" % offset)
|
||
|
return inner
|
||
|
|
||
|
def indent_region_event(self, event):
|
||
|
head, tail, chars, lines = self.get_region()
|
||
|
for pos in range(len(lines)):
|
||
|
line = lines[pos]
|
||
|
if line:
|
||
|
raw, effective = classifyws(line, self.tabwidth)
|
||
|
effective = effective + self.indentwidth
|
||
|
lines[pos] = self._make_blanks(effective) + line[raw:]
|
||
|
self.set_region(head, tail, chars, lines)
|
||
|
return "break"
|
||
|
|
||
|
def dedent_region_event(self, event):
|
||
|
head, tail, chars, lines = self.get_region()
|
||
|
for pos in range(len(lines)):
|
||
|
line = lines[pos]
|
||
|
if line:
|
||
|
raw, effective = classifyws(line, self.tabwidth)
|
||
|
effective = max(effective - self.indentwidth, 0)
|
||
|
lines[pos] = self._make_blanks(effective) + line[raw:]
|
||
|
self.set_region(head, tail, chars, lines)
|
||
|
return "break"
|
||
|
|
||
|
def comment_region_event(self, event):
|
||
|
head, tail, chars, lines = self.get_region()
|
||
|
for pos in range(len(lines) - 1):
|
||
|
line = lines[pos]
|
||
|
lines[pos] = '##' + line
|
||
|
self.set_region(head, tail, chars, lines)
|
||
|
|
||
|
def uncomment_region_event(self, event):
|
||
|
head, tail, chars, lines = self.get_region()
|
||
|
for pos in range(len(lines)):
|
||
|
line = lines[pos]
|
||
|
if not line:
|
||
|
continue
|
||
|
if line[:2] == '##':
|
||
|
line = line[2:]
|
||
|
elif line[:1] == '#':
|
||
|
line = line[1:]
|
||
|
lines[pos] = line
|
||
|
self.set_region(head, tail, chars, lines)
|
||
|
|
||
|
def tabify_region_event(self, event):
|
||
|
head, tail, chars, lines = self.get_region()
|
||
|
tabwidth = self._asktabwidth()
|
||
|
for pos in range(len(lines)):
|
||
|
line = lines[pos]
|
||
|
if line:
|
||
|
raw, effective = classifyws(line, tabwidth)
|
||
|
ntabs, nspaces = divmod(effective, tabwidth)
|
||
|
lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
|
||
|
self.set_region(head, tail, chars, lines)
|
||
|
|
||
|
def untabify_region_event(self, event):
|
||
|
head, tail, chars, lines = self.get_region()
|
||
|
tabwidth = self._asktabwidth()
|
||
|
for pos in range(len(lines)):
|
||
|
lines[pos] = lines[pos].expandtabs(tabwidth)
|
||
|
self.set_region(head, tail, chars, lines)
|
||
|
|
||
|
def toggle_tabs_event(self, event):
|
||
|
if self.editwin.askyesno(
|
||
|
"Toggle tabs",
|
||
|
"Turn tabs " + ("on", "off")[self.usetabs] + "?",
|
||
|
parent=self.text):
|
||
|
self.usetabs = not self.usetabs
|
||
|
return "break"
|
||
|
|
||
|
# XXX this isn't bound to anything -- see class tabwidth comments
|
||
|
def change_tabwidth_event(self, event):
|
||
|
new = self._asktabwidth()
|
||
|
if new != self.tabwidth:
|
||
|
self.tabwidth = new
|
||
|
self.set_indentation_params(0, guess=0)
|
||
|
return "break"
|
||
|
|
||
|
def change_indentwidth_event(self, event):
|
||
|
new = self.editwin.askinteger(
|
||
|
"Indent width",
|
||
|
"New indent width (1-16)",
|
||
|
parent=self.text,
|
||
|
initialvalue=self.indentwidth,
|
||
|
minvalue=1,
|
||
|
maxvalue=16)
|
||
|
if new and new != self.indentwidth:
|
||
|
self.indentwidth = new
|
||
|
return "break"
|
||
|
|
||
|
def get_region(self):
|
||
|
text = self.text
|
||
|
first, last = self.editwin.get_selection_indices()
|
||
|
if first and last:
|
||
|
head = text.index(first + " linestart")
|
||
|
tail = text.index(last + "-1c lineend +1c")
|
||
|
else:
|
||
|
head = text.index("insert linestart")
|
||
|
tail = text.index("insert lineend +1c")
|
||
|
chars = text.get(head, tail)
|
||
|
lines = chars.split("\n")
|
||
|
return head, tail, chars, lines
|
||
|
|
||
|
def set_region(self, head, tail, chars, lines):
|
||
|
text = self.text
|
||
|
newchars = "\n".join(lines)
|
||
|
if newchars == chars:
|
||
|
text.bell()
|
||
|
return
|
||
|
text.tag_remove("sel", "1.0", "end")
|
||
|
text.mark_set("insert", head)
|
||
|
text.undo_block_start()
|
||
|
text.delete(head, tail)
|
||
|
text.insert(head, newchars)
|
||
|
text.undo_block_stop()
|
||
|
text.tag_add("sel", head, "insert")
|
||
|
|
||
|
# Make string that displays as n leading blanks.
|
||
|
|
||
|
def _make_blanks(self, n):
|
||
|
if self.usetabs:
|
||
|
ntabs, nspaces = divmod(n, self.tabwidth)
|
||
|
return '\t' * ntabs + ' ' * nspaces
|
||
|
else:
|
||
|
return ' ' * n
|
||
|
|
||
|
# Delete from beginning of line to insert point, then reinsert
|
||
|
# column logical (meaning use tabs if appropriate) spaces.
|
||
|
|
||
|
def reindent_to(self, column):
|
||
|
text = self.text
|
||
|
text.undo_block_start()
|
||
|
if text.compare("insert linestart", "!=", "insert"):
|
||
|
text.delete("insert linestart", "insert")
|
||
|
if column:
|
||
|
text.insert("insert", self._make_blanks(column))
|
||
|
text.undo_block_stop()
|
||
|
|
||
|
def _asktabwidth(self):
|
||
|
return self.editwin.askinteger(
|
||
|
"Tab width",
|
||
|
"Spaces per tab?",
|
||
|
parent=self.text,
|
||
|
initialvalue=self.tabwidth,
|
||
|
minvalue=1,
|
||
|
maxvalue=16) or self.tabwidth
|
||
|
|
||
|
# Guess indentwidth from text content.
|
||
|
# Return guessed indentwidth. This should not be believed unless
|
||
|
# it's in a reasonable range (e.g., it will be 0 if no indented
|
||
|
# blocks are found).
|
||
|
|
||
|
def guess_indent(self):
|
||
|
opener, indented = IndentSearcher(self.text, self.tabwidth).run()
|
||
|
if opener and indented:
|
||
|
raw, indentsmall = classifyws(opener, self.tabwidth)
|
||
|
raw, indentlarge = classifyws(indented, self.tabwidth)
|
||
|
else:
|
||
|
indentsmall = indentlarge = 0
|
||
|
return indentlarge - indentsmall
|
||
|
|
||
|
# "line.col" -> line, as an int
|
||
|
def index2line(index):
|
||
|
return int(float(index))
|
||
|
|
||
|
# Look at the leading whitespace in s.
|
||
|
# Return pair (# of leading ws characters,
|
||
|
# effective # of leading blanks after expanding
|
||
|
# tabs to width tabwidth)
|
||
|
|
||
|
def classifyws(s, tabwidth):
|
||
|
raw = effective = 0
|
||
|
for ch in s:
|
||
|
if ch == ' ':
|
||
|
raw = raw + 1
|
||
|
effective = effective + 1
|
||
|
elif ch == '\t':
|
||
|
raw = raw + 1
|
||
|
effective = (effective // tabwidth + 1) * tabwidth
|
||
|
else:
|
||
|
break
|
||
|
return raw, effective
|
||
|
|
||
|
class IndentSearcher:
|
||
|
|
||
|
# .run() chews over the Text widget, looking for a block opener
|
||
|
# and the stmt following it. Returns a pair,
|
||
|
# (line containing block opener, line containing stmt)
|
||
|
# Either or both may be None.
|
||
|
|
||
|
def __init__(self, text, tabwidth):
|
||
|
self.text = text
|
||
|
self.tabwidth = tabwidth
|
||
|
self.i = self.finished = 0
|
||
|
self.blkopenline = self.indentedline = None
|
||
|
|
||
|
def readline(self):
|
||
|
if self.finished:
|
||
|
val = ""
|
||
|
else:
|
||
|
i = self.i = self.i + 1
|
||
|
mark = repr(i) + ".0"
|
||
|
if self.text.compare(mark, ">=", "end"):
|
||
|
val = ""
|
||
|
else:
|
||
|
val = self.text.get(mark, mark + " lineend+1c")
|
||
|
# hrm - not sure this is correct in py3k - the source code may have
|
||
|
# an encoding declared, but the data will *always* be in
|
||
|
# default_scintilla_encoding - so if anyone looks at the encoding decl
|
||
|
# in the source they will be wrong. I think. Maybe. Or something...
|
||
|
return val.encode(default_scintilla_encoding)
|
||
|
|
||
|
def run(self):
|
||
|
OPENERS=('class', 'def', 'for', 'if', 'try', 'while')
|
||
|
INDENT=tokenize.INDENT
|
||
|
NAME=tokenize.NAME
|
||
|
|
||
|
save_tabsize = tokenize.tabsize
|
||
|
tokenize.tabsize = self.tabwidth
|
||
|
try:
|
||
|
try:
|
||
|
for (typ, token, start, end, line) in token_generator(self.readline):
|
||
|
if typ == NAME and token in OPENERS:
|
||
|
self.blkopenline = line
|
||
|
elif typ == INDENT and self.blkopenline:
|
||
|
self.indentedline = line
|
||
|
break
|
||
|
|
||
|
except (tokenize.TokenError, IndentationError):
|
||
|
# since we cut off the tokenizer early, we can trigger
|
||
|
# spurious errors
|
||
|
pass
|
||
|
finally:
|
||
|
tokenize.tabsize = save_tabsize
|
||
|
return self.blkopenline, self.indentedline
|