mirror of
https://github.com/python/cpython.git
synced 2025-10-29 01:22:59 +00:00
Much improved autoindent and handling of tabs,
by Tim Peters.
This commit is contained in:
parent
c40c54782c
commit
def2c96718
3 changed files with 242 additions and 65 deletions
|
|
@ -1,5 +1,10 @@
|
||||||
import string
|
import string
|
||||||
from Tkinter import TclError
|
from Tkinter import TclError
|
||||||
|
import tkMessageBox
|
||||||
|
import tkSimpleDialog
|
||||||
|
|
||||||
|
# The default tab setting for a Text widget, in average-width characters.
|
||||||
|
TK_TABWIDTH_DEFAULT = 8
|
||||||
|
|
||||||
###$ event <<newline-and-indent>>
|
###$ event <<newline-and-indent>>
|
||||||
###$ win <Key-Return>
|
###$ win <Key-Return>
|
||||||
|
|
@ -58,6 +63,9 @@ class AutoIndent:
|
||||||
('U_ncomment region', '<<uncomment-region>>'),
|
('U_ncomment region', '<<uncomment-region>>'),
|
||||||
('Tabify region', '<<tabify-region>>'),
|
('Tabify region', '<<tabify-region>>'),
|
||||||
('Untabify region', '<<untabify-region>>'),
|
('Untabify region', '<<untabify-region>>'),
|
||||||
|
('Toggle tabs', '<<toggle-tabs>>'),
|
||||||
|
('New tab width', '<<change-tabwidth>>'),
|
||||||
|
('New indent width', '<<change-indentwidth>>'),
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -74,6 +82,9 @@ class AutoIndent:
|
||||||
'<<uncomment-region>>': ['<Alt-Key-4>'],
|
'<<uncomment-region>>': ['<Alt-Key-4>'],
|
||||||
'<<tabify-region>>': ['<Alt-Key-5>'],
|
'<<tabify-region>>': ['<Alt-Key-5>'],
|
||||||
'<<untabify-region>>': ['<Alt-Key-6>'],
|
'<<untabify-region>>': ['<Alt-Key-6>'],
|
||||||
|
'<<toggle-tabs>>': ['<Alt-Key-t>'],
|
||||||
|
'<<change-tabwidth>>': ['<Alt-Key-u>'],
|
||||||
|
'<<change-indentwidth>>': ['<Alt-Key-v>'],
|
||||||
}
|
}
|
||||||
|
|
||||||
unix_keydefs = {
|
unix_keydefs = {
|
||||||
|
|
@ -89,21 +100,62 @@ class AutoIndent:
|
||||||
'<<untabify-region>>': ['<Alt-Key-6>', '<Meta-Key-6>'],
|
'<<untabify-region>>': ['<Alt-Key-6>', '<Meta-Key-6>'],
|
||||||
}
|
}
|
||||||
|
|
||||||
prefertabs = 0
|
# usetabs true -> literal tab characters are used by indent and
|
||||||
spaceindent = 4*" "
|
# 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
|
||||||
|
usetabs = 0
|
||||||
|
indentwidth = 4
|
||||||
|
tabwidth = 8
|
||||||
|
|
||||||
def __init__(self, editwin):
|
def __init__(self, editwin):
|
||||||
self.text = editwin.text
|
self.text = editwin.text
|
||||||
|
|
||||||
def config(self, **options):
|
def config(self, **options):
|
||||||
for key, value in options.items():
|
for key, value in options.items():
|
||||||
if key == 'prefertabs':
|
if key == 'usetabs':
|
||||||
self.prefertabs = value
|
self.usetabs = value
|
||||||
elif key == 'spaceindent':
|
elif key == 'indentwidth':
|
||||||
self.spaceindent = value
|
self.indentwidth = value
|
||||||
|
elif key == 'tabwidth':
|
||||||
|
self.tabwidth = value
|
||||||
else:
|
else:
|
||||||
raise KeyError, "bad option name: %s" % `key`
|
raise KeyError, "bad option name: %s" % `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):
|
||||||
|
text = self.text
|
||||||
|
|
||||||
|
if guess and ispythonsource:
|
||||||
|
i = self.guess_indent()
|
||||||
|
import sys
|
||||||
|
##sys.__stdout__.write("indent %d\n" % i)
|
||||||
|
if 2 <= i <= 8:
|
||||||
|
self.indentwidth = i
|
||||||
|
if self.indentwidth != self.tabwidth:
|
||||||
|
self.usetabs = 0
|
||||||
|
|
||||||
|
current_tabs = text['tabs']
|
||||||
|
if current_tabs == "" and self.tabwidth == TK_TABWIDTH_DEFAULT:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Reconfigure the Text widget by measuring the width
|
||||||
|
# of a tabwidth-length string in pixels, forcing the
|
||||||
|
# widget's tab stops to that.
|
||||||
|
need_tabs = text.tk.call("font", "measure", text['font'],
|
||||||
|
"-displayof", text.master,
|
||||||
|
"n" * self.tabwidth)
|
||||||
|
if current_tabs != need_tabs:
|
||||||
|
text.configure(tabs=need_tabs)
|
||||||
|
|
||||||
def smart_backspace_event(self, event):
|
def smart_backspace_event(self, event):
|
||||||
text = self.text
|
text = self.text
|
||||||
try:
|
try:
|
||||||
|
|
@ -115,16 +167,15 @@ class AutoIndent:
|
||||||
text.delete(first, last)
|
text.delete(first, last)
|
||||||
text.mark_set("insert", first)
|
text.mark_set("insert", first)
|
||||||
return "break"
|
return "break"
|
||||||
# After Tim Peters
|
# If we're at the end of leading whitespace, nuke one indent
|
||||||
ndelete = 1
|
# level, else one character.
|
||||||
chars = text.get("insert linestart", "insert")
|
chars = text.get("insert linestart", "insert")
|
||||||
i = 0
|
raw, effective = classifyws(chars, self.tabwidth)
|
||||||
n = len(chars)
|
if 0 < raw == len(chars):
|
||||||
while i < n and chars[i] in " \t":
|
if effective >= self.indentwidth:
|
||||||
i = i+1
|
self.reindent_to(effective - self.indentwidth)
|
||||||
if i == n and chars[-4:] == " ":
|
return "break"
|
||||||
ndelete = 4
|
text.delete("insert-1c")
|
||||||
text.delete("insert - %d chars" % ndelete, "insert")
|
|
||||||
return "break"
|
return "break"
|
||||||
|
|
||||||
def smart_indent_event(self, event):
|
def smart_indent_event(self, event):
|
||||||
|
|
@ -132,10 +183,7 @@ class AutoIndent:
|
||||||
# delete it
|
# delete it
|
||||||
# elif multiline selection:
|
# elif multiline selection:
|
||||||
# do indent-region & return
|
# do indent-region & return
|
||||||
# if tabs preferred:
|
# indent one level
|
||||||
# insert a tab
|
|
||||||
# else:
|
|
||||||
# insert spaces up to next higher multiple of indent level
|
|
||||||
text = self.text
|
text = self.text
|
||||||
try:
|
try:
|
||||||
first = text.index("sel.first")
|
first = text.index("sel.first")
|
||||||
|
|
@ -149,12 +197,19 @@ class AutoIndent:
|
||||||
return self.indent_region_event(event)
|
return self.indent_region_event(event)
|
||||||
text.delete(first, last)
|
text.delete(first, last)
|
||||||
text.mark_set("insert", first)
|
text.mark_set("insert", first)
|
||||||
if self.prefertabs:
|
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'
|
pad = '\t'
|
||||||
else:
|
else:
|
||||||
n = len(self.spaceindent)
|
effective = len(string.expandtabs(prefix,
|
||||||
prefix = text.get("insert linestart", "insert")
|
self.tabwidth))
|
||||||
pad = ' ' * (n - len(prefix) % n)
|
n = self.indentwidth
|
||||||
|
pad = ' ' * (n - effective % n)
|
||||||
text.insert("insert", pad)
|
text.insert("insert", pad)
|
||||||
text.see("insert")
|
text.see("insert")
|
||||||
return "break"
|
return "break"
|
||||||
|
|
@ -185,10 +240,13 @@ class AutoIndent:
|
||||||
i = i + 1
|
i = i + 1
|
||||||
if i:
|
if i:
|
||||||
text.delete("insert - %d chars" % i, "insert")
|
text.delete("insert - %d chars" % i, "insert")
|
||||||
|
# XXX this reproduces the current line's indentation,
|
||||||
|
# without regard for usetabs etc; could instead insert
|
||||||
|
# "\n" + self._make_blanks(classifyws(indent)[1]).
|
||||||
text.insert("insert", "\n" + indent)
|
text.insert("insert", "\n" + indent)
|
||||||
if _is_block_opener(line):
|
if _is_block_opener(line):
|
||||||
self.smart_indent_event(event)
|
self.smart_indent_event(event)
|
||||||
elif indent and _is_block_closer(line) and line[-1:] != "\\":
|
elif indent and _is_block_closer(line) and line[-1] != "\\":
|
||||||
self.smart_backspace_event(event)
|
self.smart_backspace_event(event)
|
||||||
text.see("insert")
|
text.see("insert")
|
||||||
return "break"
|
return "break"
|
||||||
|
|
@ -202,11 +260,9 @@ class AutoIndent:
|
||||||
for pos in range(len(lines)):
|
for pos in range(len(lines)):
|
||||||
line = lines[pos]
|
line = lines[pos]
|
||||||
if line:
|
if line:
|
||||||
i, n = 0, len(line)
|
raw, effective = classifyws(line, self.tabwidth)
|
||||||
while i < n and line[i] in " \t":
|
effective = effective + self.indentwidth
|
||||||
i = i+1
|
lines[pos] = self._make_blanks(effective) + line[raw:]
|
||||||
line = line[:i] + " " + line[i:]
|
|
||||||
lines[pos] = line
|
|
||||||
self.set_region(head, tail, chars, lines)
|
self.set_region(head, tail, chars, lines)
|
||||||
return "break"
|
return "break"
|
||||||
|
|
||||||
|
|
@ -215,20 +271,9 @@ class AutoIndent:
|
||||||
for pos in range(len(lines)):
|
for pos in range(len(lines)):
|
||||||
line = lines[pos]
|
line = lines[pos]
|
||||||
if line:
|
if line:
|
||||||
i, n = 0, len(line)
|
raw, effective = classifyws(line, self.tabwidth)
|
||||||
while i < n and line[i] in " \t":
|
effective = max(effective - self.indentwidth, 0)
|
||||||
i = i+1
|
lines[pos] = self._make_blanks(effective) + line[raw:]
|
||||||
indent, line = line[:i], line[i:]
|
|
||||||
if indent:
|
|
||||||
if indent == "\t" or indent[-2:] == "\t\t":
|
|
||||||
indent = indent[:-1] + " "
|
|
||||||
elif indent[-4:] == " ":
|
|
||||||
indent = indent[:-4]
|
|
||||||
else:
|
|
||||||
indent = string.expandtabs(indent, 8)
|
|
||||||
indent = indent[:-4]
|
|
||||||
line = indent + line
|
|
||||||
lines[pos] = line
|
|
||||||
self.set_region(head, tail, chars, lines)
|
self.set_region(head, tail, chars, lines)
|
||||||
return "break"
|
return "break"
|
||||||
|
|
||||||
|
|
@ -236,8 +281,7 @@ class AutoIndent:
|
||||||
head, tail, chars, lines = self.get_region()
|
head, tail, chars, lines = self.get_region()
|
||||||
for pos in range(len(lines)):
|
for pos in range(len(lines)):
|
||||||
line = lines[pos]
|
line = lines[pos]
|
||||||
if not line:
|
if line:
|
||||||
continue
|
|
||||||
lines[pos] = '##' + line
|
lines[pos] = '##' + line
|
||||||
self.set_region(head, tail, chars, lines)
|
self.set_region(head, tail, chars, lines)
|
||||||
|
|
||||||
|
|
@ -256,14 +300,48 @@ class AutoIndent:
|
||||||
|
|
||||||
def tabify_region_event(self, event):
|
def tabify_region_event(self, event):
|
||||||
head, tail, chars, lines = self.get_region()
|
head, tail, chars, lines = self.get_region()
|
||||||
lines = map(tabify, lines)
|
for pos in range(len(lines)):
|
||||||
|
line = lines[pos]
|
||||||
|
if line:
|
||||||
|
raw, effective = classifyws(line, self.tabwidth)
|
||||||
|
ntabs, nspaces = divmod(effective, self.tabwidth)
|
||||||
|
lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
|
||||||
self.set_region(head, tail, chars, lines)
|
self.set_region(head, tail, chars, lines)
|
||||||
|
|
||||||
def untabify_region_event(self, event):
|
def untabify_region_event(self, event):
|
||||||
head, tail, chars, lines = self.get_region()
|
head, tail, chars, lines = self.get_region()
|
||||||
lines = map(string.expandtabs, lines)
|
for pos in range(len(lines)):
|
||||||
|
lines[pos] = string.expandtabs(lines[pos], self.tabwidth)
|
||||||
self.set_region(head, tail, chars, lines)
|
self.set_region(head, tail, chars, lines)
|
||||||
|
|
||||||
|
def toggle_tabs_event(self, event):
|
||||||
|
if tkMessageBox.askyesno("Toggle tabs",
|
||||||
|
"Turn tabs " + ("on", "off")[self.usetabs] + "?",
|
||||||
|
parent=self.text):
|
||||||
|
self.usetabs = not self.usetabs
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
def change_tabwidth_event(self, event):
|
||||||
|
new = tkSimpleDialog.askinteger("Tab width",
|
||||||
|
"New tab width (2-16)",
|
||||||
|
parent=self.text,
|
||||||
|
initialvalue=self.tabwidth,
|
||||||
|
minvalue=2, maxvalue=16)
|
||||||
|
if new and new != self.tabwidth:
|
||||||
|
self.tabwidth = new
|
||||||
|
self.set_indentation_params(0, guess=0)
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
def change_indentwidth_event(self, event):
|
||||||
|
new = tkSimpleDialog.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):
|
def get_region(self):
|
||||||
text = self.text
|
text = self.text
|
||||||
head = text.index("sel.first linestart")
|
head = text.index("sel.first linestart")
|
||||||
|
|
@ -289,15 +367,110 @@ class AutoIndent:
|
||||||
text.undo_block_stop()
|
text.undo_block_stop()
|
||||||
text.tag_add("sel", head, "insert")
|
text.tag_add("sel", head, "insert")
|
||||||
|
|
||||||
def tabify(line, tabsize=8):
|
# Make string that displays as n leading blanks.
|
||||||
spaces = tabsize * ' '
|
|
||||||
for i in range(0, len(line), tabsize):
|
def _make_blanks(self, n):
|
||||||
if line[i:i+tabsize] != spaces:
|
if self.usetabs:
|
||||||
break
|
ntabs, nspaces = divmod(n, self.tabwidth)
|
||||||
|
return '\t' * ntabs + ' ' * nspaces
|
||||||
else:
|
else:
|
||||||
i = len(line)
|
return ' ' * n
|
||||||
return '\t' * (i/tabsize) + line[i:]
|
|
||||||
|
# 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()
|
||||||
|
text.delete("insert linestart", "insert")
|
||||||
|
if column:
|
||||||
|
text.insert("insert", self._make_blanks(column))
|
||||||
|
text.undo_block_stop()
|
||||||
|
|
||||||
|
# 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
|
# "line.col" -> line, as an int
|
||||||
def index2line(index):
|
def index2line(index):
|
||||||
return int(float(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
|
||||||
|
|
||||||
|
import tokenize
|
||||||
|
_tokenize = tokenize
|
||||||
|
del tokenize
|
||||||
|
|
||||||
|
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:
|
||||||
|
return ""
|
||||||
|
i = self.i = self.i + 1
|
||||||
|
mark = `i` + ".0"
|
||||||
|
if self.text.compare(mark, ">=", "end"):
|
||||||
|
return ""
|
||||||
|
return self.text.get(mark, mark + " lineend+1c")
|
||||||
|
|
||||||
|
def tokeneater(self, type, token, start, end, line,
|
||||||
|
INDENT=_tokenize.INDENT,
|
||||||
|
NAME=_tokenize.NAME,
|
||||||
|
OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
|
||||||
|
if self.finished:
|
||||||
|
pass
|
||||||
|
elif type == NAME and token in OPENERS:
|
||||||
|
self.blkopenline = line
|
||||||
|
elif type == INDENT and self.blkopenline:
|
||||||
|
self.indentedline = line
|
||||||
|
self.finished = 1
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
save_tabsize = _tokenize.tabsize
|
||||||
|
_tokenize.tabsize = self.tabwidth
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
_tokenize.tokenize(self.readline, self.tokeneater)
|
||||||
|
except _tokenize.TokenError:
|
||||||
|
# since we cut off the tokenizer early, we can trigger
|
||||||
|
# spurious errors
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
_tokenize.tabsize = save_tabsize
|
||||||
|
return self.blkopenline, self.indentedline
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ class EditorWindow:
|
||||||
text['yscrollcommand'] = vbar.set
|
text['yscrollcommand'] = vbar.set
|
||||||
if sys.platform[:3] == 'win':
|
if sys.platform[:3] == 'win':
|
||||||
text['font'] = ("lucida console", 8)
|
text['font'] = ("lucida console", 8)
|
||||||
|
# text['font'] = ("courier new", 10)
|
||||||
text.pack(side=LEFT, fill=BOTH, expand=1)
|
text.pack(side=LEFT, fill=BOTH, expand=1)
|
||||||
text.focus_set()
|
text.focus_set()
|
||||||
|
|
||||||
|
|
@ -173,6 +174,10 @@ class EditorWindow:
|
||||||
self.wmenu_end = end
|
self.wmenu_end = end
|
||||||
WindowList.register_callback(self.postwindowsmenu)
|
WindowList.register_callback(self.postwindowsmenu)
|
||||||
|
|
||||||
|
if self.extensions.has_key('AutoIndent'):
|
||||||
|
self.extensions['AutoIndent'].set_indentation_params(
|
||||||
|
self.ispythonsource(filename))
|
||||||
|
|
||||||
def wakeup(self):
|
def wakeup(self):
|
||||||
if self.top.wm_state() == "iconic":
|
if self.top.wm_state() == "iconic":
|
||||||
self.top.wm_deiconify()
|
self.top.wm_deiconify()
|
||||||
|
|
@ -575,7 +580,6 @@ class EditorWindow:
|
||||||
self.vars[name] = var = vartype(self.text)
|
self.vars[name] = var = vartype(self.text)
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
def prepstr(s):
|
def prepstr(s):
|
||||||
# Helper to extract the underscore from a string,
|
# Helper to extract the underscore from a string,
|
||||||
# e.g. prepstr("Co_py") returns (2, "Copy").
|
# e.g. prepstr("Co_py") returns (2, "Copy").
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@ class PyShell(OutputWindow):
|
||||||
__builtin__.quit = __builtin__.exit = "To exit, type Ctrl-D."
|
__builtin__.quit = __builtin__.exit = "To exit, type Ctrl-D."
|
||||||
|
|
||||||
self.auto = self.extensions["AutoIndent"] # Required extension
|
self.auto = self.extensions["AutoIndent"] # Required extension
|
||||||
self.auto.config(prefertabs=1)
|
self.auto.config(usetabs=1, indentwidth=8)
|
||||||
|
|
||||||
text = self.text
|
text = self.text
|
||||||
text.configure(wrap="char")
|
text.configure(wrap="char")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue