codekingpro/portable-devtools
114k
1import sys2import tokenize3 4from pywin import default_scintilla_encoding5 6from . import PyParse7 8if sys.version_info < (3,):9 # in py2k, tokenize() takes a 'token eater' callback, while10 # generate_tokens is a generator that works with str objects.11 token_generator = tokenize.generate_tokens12else:13 # in py3k tokenize() is the generator working with 'byte' objects, and14 # token_generator is the 'undocumented b/w compat' function that15 # theoretically works with str objects - but actually seems to fail)16 token_generator = tokenize.tokenize17 18 19class AutoIndent:20 menudefs = [21 (22 "edit",23 [24 None,25 ("_Indent region", "<<indent-region>>"),26 ("_Dedent region", "<<dedent-region>>"),27 ("Comment _out region", "<<comment-region>>"),28 ("U_ncomment region", "<<uncomment-region>>"),29 ("Tabify region", "<<tabify-region>>"),30 ("Untabify region", "<<untabify-region>>"),31 ("Toggle tabs", "<<toggle-tabs>>"),32 ("New indent width", "<<change-indentwidth>>"),33 ],34 ),35 ]36 37 keydefs = {38 "<<smart-backspace>>": ["<Key-BackSpace>"],39 "<<newline-and-indent>>": ["<Key-Return>", "<KP_Enter>"],40 "<<smart-indent>>": ["<Key-Tab>"],41 }42 43 windows_keydefs = {44 "<<indent-region>>": ["<Control-bracketright>"],45 "<<dedent-region>>": ["<Control-bracketleft>"],46 "<<comment-region>>": ["<Alt-Key-3>"],47 "<<uncomment-region>>": ["<Alt-Key-4>"],48 "<<tabify-region>>": ["<Alt-Key-5>"],49 "<<untabify-region>>": ["<Alt-Key-6>"],50 "<<toggle-tabs>>": ["<Alt-Key-t>"],51 "<<change-indentwidth>>": ["<Alt-Key-u>"],52 }53 54 unix_keydefs = {55 "<<indent-region>>": [56 "<Alt-bracketright>",57 "<Meta-bracketright>",58 "<Control-bracketright>",59 ],60 "<<dedent-region>>": [61 "<Alt-bracketleft>",62 "<Meta-bracketleft>",63 "<Control-bracketleft>",64 ],65 "<<comment-region>>": ["<Alt-Key-3>", "<Meta-Key-3>"],66 "<<uncomment-region>>": ["<Alt-Key-4>", "<Meta-Key-4>"],67 "<<tabify-region>>": ["<Alt-Key-5>", "<Meta-Key-5>"],68 "<<untabify-region>>": ["<Alt-Key-6>", "<Meta-Key-6>"],69 "<<toggle-tabs>>": ["<Alt-Key-t>"],70 "<<change-indentwidth>>": ["<Alt-Key-u>"],71 }72 73 # usetabs true -> literal tab characters are used by indent and74 # dedent cmds, possibly mixed with spaces if75 # indentwidth is not a multiple of tabwidth76 # false -> tab characters are converted to spaces by indent77 # and dedent cmds, and ditto TAB keystrokes78 # indentwidth is the number of characters per logical indent level.79 # tabwidth is the display width of a literal tab character.80 # CAUTION: telling Tk to use anything other than its default81 # tab setting causes it to use an entirely different tabbing algorithm,82 # treating tab stops as fixed distances from the left margin.83 # Nobody expects this, so for now tabwidth should never be changed.84 usetabs = 185 indentwidth = 486 tabwidth = 8 # for IDLE use, must remain 8 until Tk is fixed87 88 # If context_use_ps1 is true, parsing searches back for a ps1 line;89 # else searches for a popular (if, def, ...) Python stmt.90 context_use_ps1 = 091 92 # When searching backwards for a reliable place to begin parsing,93 # first start num_context_lines[0] lines back, then94 # num_context_lines[1] lines back if that didn't work, and so on.95 # The last value should be huge (larger than the # of lines in a96 # conceivable file).97 # Making the initial values larger slows things down more often.98 num_context_lines = 50, 500, 500000099 100 def __init__(self, editwin):101 self.editwin = editwin102 self.text = editwin.text103 104 def config(self, **options):105 for key, value in options.items():106 if key == "usetabs":107 self.usetabs = value108 elif key == "indentwidth":109 self.indentwidth = value110 elif key == "tabwidth":111 self.tabwidth = value112 elif key == "context_use_ps1":113 self.context_use_ps1 = value114 else:115 raise KeyError("bad option name: %s" % repr(key))116 117 # If ispythonsource and guess are true, guess a good value for118 # indentwidth based on file content (if possible), and if119 # indentwidth != tabwidth set usetabs false.120 # In any case, adjust the Text widget's view of what a tab121 # character means.122 123 def set_indentation_params(self, ispythonsource, guess=1):124 if guess and ispythonsource:125 i = self.guess_indent()126 if 2 <= i <= 8:127 self.indentwidth = i128 if self.indentwidth != self.tabwidth:129 self.usetabs = 0130 131 self.editwin.set_tabwidth(self.tabwidth)132 133 def smart_backspace_event(self, event):134 text = self.text135 first, last = self.editwin.get_selection_indices()136 if first and last:137 text.delete(first, last)138 text.mark_set("insert", first)139 return "break"140 # Delete whitespace left, until hitting a real char or closest141 # preceding virtual tab stop.142 chars = text.get("insert linestart", "insert")143 if chars == "":144 if text.compare("insert", ">", "1.0"):145 # easy: delete preceding newline146 text.delete("insert-1c")147 else:148 text.bell() # at start of buffer149 return "break"150 if chars[-1] not in " \t":151 # easy: delete preceding real char152 text.delete("insert-1c")153 return "break"154 # Ick. It may require *inserting* spaces if we back up over a155 # tab character! This is written to be clear, not fast.156 have = len(chars.expandtabs(self.tabwidth))157 assert have > 0158 want = int((have - 1) / self.indentwidth) * self.indentwidth159 ncharsdeleted = 0160 while 1:161 chars = chars[:-1]162 ncharsdeleted = ncharsdeleted + 1163 have = len(chars.expandtabs(self.tabwidth))164 if have <= want or chars[-1] not in " \t":165 break166 text.undo_block_start()167 text.delete("insert-%dc" % ncharsdeleted, "insert")168 if have < want:169 text.insert("insert", " " * (want - have))170 text.undo_block_stop()171 return "break"172 173 def smart_indent_event(self, event):174 # if intraline selection:175 # delete it176 # elif multiline selection:177 # do indent-region & return178 # indent one level179 text = self.text180 first, last = self.editwin.get_selection_indices()181 text.undo_block_start()182 try:183 if first and last:184 if index2line(first) != index2line(last):185 return self.indent_region_event(event)186 text.delete(first, last)187 text.mark_set("insert", first)188 prefix = text.get("insert linestart", "insert")189 raw, effective = classifyws(prefix, self.tabwidth)190 if raw == len(prefix):191 # only whitespace to the left192 self.reindent_to(effective + self.indentwidth)193 else:194 if self.usetabs:195 pad = "\t"196 else:197 effective = len(prefix.expandtabs(self.tabwidth))198 n = self.indentwidth199 pad = " " * (n - effective % n)200 text.insert("insert", pad)201 text.see("insert")202 return "break"203 finally:204 text.undo_block_stop()205 206 def newline_and_indent_event(self, event):207 text = self.text208 first, last = self.editwin.get_selection_indices()209 text.undo_block_start()210 try:211 if first and last:212 text.delete(first, last)213 text.mark_set("insert", first)214 line = text.get("insert linestart", "insert")215 i, n = 0, len(line)216 while i < n and line[i] in " \t":217 i = i + 1218 if i == n:219 # the cursor is in or at leading indentation; just inject220 # an empty line at the start and strip space from current line221 text.delete("insert - %d chars" % i, "insert")222 text.insert("insert linestart", "\n")223 return "break"224 indent = line[:i]225 # strip whitespace before insert point226 i = 0227 while line and line[-1] in " \t":228 line = line[:-1]229 i = i + 1230 if i:231 text.delete("insert - %d chars" % i, "insert")232 # strip whitespace after insert point233 while text.get("insert") in " \t":234 text.delete("insert")235 # start new line236 text.insert("insert", "\n")237 238 # adjust indentation for continuations and block239 # open/close first need to find the last stmt240 lno = index2line(text.index("insert"))241 y = PyParse.Parser(self.indentwidth, self.tabwidth)242 for context in self.num_context_lines:243 startat = max(lno - context, 1)244 startatindex = repr(startat) + ".0"245 rawtext = text.get(startatindex, "insert")246 y.set_str(rawtext)247 bod = y.find_good_parse_start(248 self.context_use_ps1, self._build_char_in_string_func(startatindex)249 )250 if bod is not None or startat == 1:251 break252 y.set_lo(bod or 0)253 c = y.get_continuation_type()254 if c != PyParse.C_NONE:255 # The current stmt hasn't ended yet.256 if c == PyParse.C_STRING:257 # inside a string; just mimic the current indent258 text.insert("insert", indent)259 elif c == PyParse.C_BRACKET:260 # line up with the first (if any) element of the261 # last open bracket structure; else indent one262 # level beyond the indent of the line with the263 # last open bracket264 self.reindent_to(y.compute_bracket_indent())265 elif c == PyParse.C_BACKSLASH:266 # if more than one line in this stmt already, just267 # mimic the current indent; else if initial line268 # has a start on an assignment stmt, indent to269 # beyond leftmost =; else to beyond first chunk of270 # non-whitespace on initial line271 if y.get_num_lines_in_stmt() > 1:272 text.insert("insert", indent)273 else:274 self.reindent_to(y.compute_backslash_indent())275 else:276 assert 0, "bogus continuation type " + repr(c)277 return "break"278 279 # This line starts a brand new stmt; indent relative to280 # indentation of initial line of closest preceding281 # interesting stmt.282 indent = y.get_base_indent_string()283 text.insert("insert", indent)284 if y.is_block_opener():285 self.smart_indent_event(event)286 elif indent and y.is_block_closer():287 self.smart_backspace_event(event)288 return "break"289 finally:290 text.see("insert")291 text.undo_block_stop()292 293 auto_indent = newline_and_indent_event294 295 # Our editwin provides a is_char_in_string function that works296 # with a Tk text index, but PyParse only knows about offsets into297 # a string. This builds a function for PyParse that accepts an298 # offset.299 300 def _build_char_in_string_func(self, startindex):301 def inner(offset, _startindex=startindex, _icis=self.editwin.is_char_in_string):302 return _icis(_startindex + "+%dc" % offset)303 304 return inner305 306 def indent_region_event(self, event):307 head, tail, chars, lines = self.get_region()308 for pos in range(len(lines)):309 line = lines[pos]310 if line:311 raw, effective = classifyws(line, self.tabwidth)312 effective = effective + self.indentwidth313 lines[pos] = self._make_blanks(effective) + line[raw:]314 self.set_region(head, tail, chars, lines)315 return "break"316 317 def dedent_region_event(self, event):318 head, tail, chars, lines = self.get_region()319 for pos in range(len(lines)):320 line = lines[pos]321 if line:322 raw, effective = classifyws(line, self.tabwidth)323 effective = max(effective - self.indentwidth, 0)324 lines[pos] = self._make_blanks(effective) + line[raw:]325 self.set_region(head, tail, chars, lines)326 return "break"327 328 def comment_region_event(self, event):329 head, tail, chars, lines = self.get_region()330 for pos in range(len(lines) - 1):331 line = lines[pos]332 lines[pos] = "##" + line333 self.set_region(head, tail, chars, lines)334 335 def uncomment_region_event(self, event):336 head, tail, chars, lines = self.get_region()337 for pos in range(len(lines)):338 line = lines[pos]339 if not line:340 continue341 if line[:2] == "##":342 line = line[2:]343 elif line[:1] == "#":344 line = line[1:]345 lines[pos] = line346 self.set_region(head, tail, chars, lines)347 348 def tabify_region_event(self, event):349 head, tail, chars, lines = self.get_region()350 tabwidth = self._asktabwidth()351 for pos in range(len(lines)):352 line = lines[pos]353 if line:354 raw, effective = classifyws(line, tabwidth)355 ntabs, nspaces = divmod(effective, tabwidth)356 lines[pos] = "\t" * ntabs + " " * nspaces + line[raw:]357 self.set_region(head, tail, chars, lines)358 359 def untabify_region_event(self, event):360 head, tail, chars, lines = self.get_region()361 tabwidth = self._asktabwidth()362 for pos in range(len(lines)):363 lines[pos] = lines[pos].expandtabs(tabwidth)364 self.set_region(head, tail, chars, lines)365 366 def toggle_tabs_event(self, event):367 if self.editwin.askyesno(368 "Toggle tabs",369 "Turn tabs " + ("on", "off")[self.usetabs] + "?",370 parent=self.text,371 ):372 self.usetabs = not self.usetabs373 return "break"374 375 # XXX this isn't bound to anything -- see class tabwidth comments376 def change_tabwidth_event(self, event):377 new = self._asktabwidth()378 if new != self.tabwidth:379 self.tabwidth = new380 self.set_indentation_params(0, guess=0)381 return "break"382 383 def change_indentwidth_event(self, event):384 new = self.editwin.askinteger(385 "Indent width",386 "New indent width (1-16)",387 parent=self.text,388 initialvalue=self.indentwidth,389 minvalue=1,390 maxvalue=16,391 )392 if new and new != self.indentwidth:393 self.indentwidth = new394 return "break"395 396 def get_region(self):397 text = self.text398 first, last = self.editwin.get_selection_indices()399 if first and last:400 head = text.index(first + " linestart")401 tail = text.index(last + "-1c lineend +1c")402 else:403 head = text.index("insert linestart")404 tail = text.index("insert lineend +1c")405 chars = text.get(head, tail)406 lines = chars.split("\n")407 return head, tail, chars, lines408 409 def set_region(self, head, tail, chars, lines):410 text = self.text411 newchars = "\n".join(lines)412 if newchars == chars:413 text.bell()414 return415 text.tag_remove("sel", "1.0", "end")416 text.mark_set("insert", head)417 text.undo_block_start()418 text.delete(head, tail)419 text.insert(head, newchars)420 text.undo_block_stop()421 text.tag_add("sel", head, "insert")422 423 # Make string that displays as n leading blanks.424 425 def _make_blanks(self, n):426 if self.usetabs:427 ntabs, nspaces = divmod(n, self.tabwidth)428 return "\t" * ntabs + " " * nspaces429 else:430 return " " * n431 432 # Delete from beginning of line to insert point, then reinsert433 # column logical (meaning use tabs if appropriate) spaces.434 435 def reindent_to(self, column):436 text = self.text437 text.undo_block_start()438 if text.compare("insert linestart", "!=", "insert"):439 text.delete("insert linestart", "insert")440 if column:441 text.insert("insert", self._make_blanks(column))442 text.undo_block_stop()443 444 def _asktabwidth(self):445 return (446 self.editwin.askinteger(447 "Tab width",448 "Spaces per tab?",449 parent=self.text,450 initialvalue=self.tabwidth,451 minvalue=1,452 maxvalue=16,453 )454 or self.tabwidth455 )456 457 # Guess indentwidth from text content.458 # Return guessed indentwidth. This should not be believed unless459 # it's in a reasonable range (e.g., it will be 0 if no indented460 # blocks are found).461 462 def guess_indent(self):463 opener, indented = IndentSearcher(self.text, self.tabwidth).run()464 if opener and indented:465 raw, indentsmall = classifyws(opener, self.tabwidth)466 raw, indentlarge = classifyws(indented, self.tabwidth)467 else:468 indentsmall = indentlarge = 0469 return indentlarge - indentsmall470 471 472# "line.col" -> line, as an int473def index2line(index):474 return int(float(index))475 476 477# Look at the leading whitespace in s.478# Return pair (# of leading ws characters,479# effective # of leading blanks after expanding480# tabs to width tabwidth)481 482 483def classifyws(s, tabwidth):484 raw = effective = 0485 for ch in s:486 if ch == " ":487 raw = raw + 1488 effective = effective + 1489 elif ch == "\t":490 raw = raw + 1491 effective = (effective // tabwidth + 1) * tabwidth492 else:493 break494 return raw, effective495 496 497class IndentSearcher:498 # .run() chews over the Text widget, looking for a block opener499 # and the stmt following it. Returns a pair,500 # (line containing block opener, line containing stmt)501 # Either or both may be None.502 503 def __init__(self, text, tabwidth):504 self.text = text505 self.tabwidth = tabwidth506 self.i = self.finished = 0507 self.blkopenline = self.indentedline = None508 509 def readline(self):510 if self.finished:511 val = ""512 else:513 i = self.i = self.i + 1514 mark = repr(i) + ".0"515 if self.text.compare(mark, ">=", "end"):516 val = ""517 else:518 val = self.text.get(mark, mark + " lineend+1c")519 # hrm - not sure this is correct in py3k - the source code may have520 # an encoding declared, but the data will *always* be in521 # default_scintilla_encoding - so if anyone looks at the encoding decl522 # in the source they will be wrong. I think. Maybe. Or something...523 return val.encode(default_scintilla_encoding)524 525 def run(self):526 OPENERS = ("class", "def", "for", "if", "try", "while")527 INDENT = tokenize.INDENT528 NAME = tokenize.NAME529 530 save_tabsize = tokenize.tabsize531 tokenize.tabsize = self.tabwidth532 try:533 try:534 for typ, token, start, end, line in token_generator(self.readline):535 if typ == NAME and token in OPENERS:536 self.blkopenline = line537 elif typ == INDENT and self.blkopenline:538 self.indentedline = line539 break540 541 except (tokenize.TokenError, IndentationError):542 # since we cut off the tokenizer early, we can trigger543 # spurious errors544 pass545 finally:546 tokenize.tabsize = save_tabsize547 return self.blkopenline, self.indentedline548 