AutoIndent.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. import sys
  2. import string, tokenize
  3. from . import PyParse
  4. from pywin import default_scintilla_encoding
  5. if sys.version_info < (3,):
  6. # in py2k, tokenize() takes a 'token eater' callback, while
  7. # generate_tokens is a generator that works with str objects.
  8. token_generator = tokenize.generate_tokens
  9. else:
  10. # in py3k tokenize() is the generator working with 'byte' objects, and
  11. # token_generator is the 'undocumented b/w compat' function that
  12. # theoretically works with str objects - but actually seems to fail)
  13. token_generator = tokenize.tokenize
  14. class AutoIndent:
  15. menudefs = [
  16. ('edit', [
  17. None,
  18. ('_Indent region', '<<indent-region>>'),
  19. ('_Dedent region', '<<dedent-region>>'),
  20. ('Comment _out region', '<<comment-region>>'),
  21. ('U_ncomment region', '<<uncomment-region>>'),
  22. ('Tabify region', '<<tabify-region>>'),
  23. ('Untabify region', '<<untabify-region>>'),
  24. ('Toggle tabs', '<<toggle-tabs>>'),
  25. ('New indent width', '<<change-indentwidth>>'),
  26. ]),
  27. ]
  28. keydefs = {
  29. '<<smart-backspace>>': ['<Key-BackSpace>'],
  30. '<<newline-and-indent>>': ['<Key-Return>', '<KP_Enter>'],
  31. '<<smart-indent>>': ['<Key-Tab>']
  32. }
  33. windows_keydefs = {
  34. '<<indent-region>>': ['<Control-bracketright>'],
  35. '<<dedent-region>>': ['<Control-bracketleft>'],
  36. '<<comment-region>>': ['<Alt-Key-3>'],
  37. '<<uncomment-region>>': ['<Alt-Key-4>'],
  38. '<<tabify-region>>': ['<Alt-Key-5>'],
  39. '<<untabify-region>>': ['<Alt-Key-6>'],
  40. '<<toggle-tabs>>': ['<Alt-Key-t>'],
  41. '<<change-indentwidth>>': ['<Alt-Key-u>'],
  42. }
  43. unix_keydefs = {
  44. '<<indent-region>>': ['<Alt-bracketright>',
  45. '<Meta-bracketright>',
  46. '<Control-bracketright>'],
  47. '<<dedent-region>>': ['<Alt-bracketleft>',
  48. '<Meta-bracketleft>',
  49. '<Control-bracketleft>'],
  50. '<<comment-region>>': ['<Alt-Key-3>', '<Meta-Key-3>'],
  51. '<<uncomment-region>>': ['<Alt-Key-4>', '<Meta-Key-4>'],
  52. '<<tabify-region>>': ['<Alt-Key-5>', '<Meta-Key-5>'],
  53. '<<untabify-region>>': ['<Alt-Key-6>', '<Meta-Key-6>'],
  54. '<<toggle-tabs>>': ['<Alt-Key-t>'],
  55. '<<change-indentwidth>>': ['<Alt-Key-u>'],
  56. }
  57. # usetabs true -> literal tab characters are used by indent and
  58. # dedent cmds, possibly mixed with spaces if
  59. # indentwidth is not a multiple of tabwidth
  60. # false -> tab characters are converted to spaces by indent
  61. # and dedent cmds, and ditto TAB keystrokes
  62. # indentwidth is the number of characters per logical indent level.
  63. # tabwidth is the display width of a literal tab character.
  64. # CAUTION: telling Tk to use anything other than its default
  65. # tab setting causes it to use an entirely different tabbing algorithm,
  66. # treating tab stops as fixed distances from the left margin.
  67. # Nobody expects this, so for now tabwidth should never be changed.
  68. usetabs = 1
  69. indentwidth = 4
  70. tabwidth = 8 # for IDLE use, must remain 8 until Tk is fixed
  71. # If context_use_ps1 is true, parsing searches back for a ps1 line;
  72. # else searches for a popular (if, def, ...) Python stmt.
  73. context_use_ps1 = 0
  74. # When searching backwards for a reliable place to begin parsing,
  75. # first start num_context_lines[0] lines back, then
  76. # num_context_lines[1] lines back if that didn't work, and so on.
  77. # The last value should be huge (larger than the # of lines in a
  78. # conceivable file).
  79. # Making the initial values larger slows things down more often.
  80. num_context_lines = 50, 500, 5000000
  81. def __init__(self, editwin):
  82. self.editwin = editwin
  83. self.text = editwin.text
  84. def config(self, **options):
  85. for key, value in options.items():
  86. if key == 'usetabs':
  87. self.usetabs = value
  88. elif key == 'indentwidth':
  89. self.indentwidth = value
  90. elif key == 'tabwidth':
  91. self.tabwidth = value
  92. elif key == 'context_use_ps1':
  93. self.context_use_ps1 = value
  94. else:
  95. raise KeyError("bad option name: %s" % repr(key))
  96. # If ispythonsource and guess are true, guess a good value for
  97. # indentwidth based on file content (if possible), and if
  98. # indentwidth != tabwidth set usetabs false.
  99. # In any case, adjust the Text widget's view of what a tab
  100. # character means.
  101. def set_indentation_params(self, ispythonsource, guess=1):
  102. if guess and ispythonsource:
  103. i = self.guess_indent()
  104. if 2 <= i <= 8:
  105. self.indentwidth = i
  106. if self.indentwidth != self.tabwidth:
  107. self.usetabs = 0
  108. self.editwin.set_tabwidth(self.tabwidth)
  109. def smart_backspace_event(self, event):
  110. text = self.text
  111. first, last = self.editwin.get_selection_indices()
  112. if first and last:
  113. text.delete(first, last)
  114. text.mark_set("insert", first)
  115. return "break"
  116. # Delete whitespace left, until hitting a real char or closest
  117. # preceding virtual tab stop.
  118. chars = text.get("insert linestart", "insert")
  119. if chars == '':
  120. if text.compare("insert", ">", "1.0"):
  121. # easy: delete preceding newline
  122. text.delete("insert-1c")
  123. else:
  124. text.bell() # at start of buffer
  125. return "break"
  126. if chars[-1] not in " \t":
  127. # easy: delete preceding real char
  128. text.delete("insert-1c")
  129. return "break"
  130. # Ick. It may require *inserting* spaces if we back up over a
  131. # tab character! This is written to be clear, not fast.
  132. have = len(chars.expandtabs(self.tabwidth))
  133. assert have > 0
  134. want = int((have - 1) / self.indentwidth) * self.indentwidth
  135. ncharsdeleted = 0
  136. while 1:
  137. chars = chars[:-1]
  138. ncharsdeleted = ncharsdeleted + 1
  139. have = len(chars.expandtabs(self.tabwidth))
  140. if have <= want or chars[-1] not in " \t":
  141. break
  142. text.undo_block_start()
  143. text.delete("insert-%dc" % ncharsdeleted, "insert")
  144. if have < want:
  145. text.insert("insert", ' ' * (want - have))
  146. text.undo_block_stop()
  147. return "break"
  148. def smart_indent_event(self, event):
  149. # if intraline selection:
  150. # delete it
  151. # elif multiline selection:
  152. # do indent-region & return
  153. # indent one level
  154. text = self.text
  155. first, last = self.editwin.get_selection_indices()
  156. text.undo_block_start()
  157. try:
  158. if first and last:
  159. if index2line(first) != index2line(last):
  160. return self.indent_region_event(event)
  161. text.delete(first, last)
  162. text.mark_set("insert", first)
  163. prefix = text.get("insert linestart", "insert")
  164. raw, effective = classifyws(prefix, self.tabwidth)
  165. if raw == len(prefix):
  166. # only whitespace to the left
  167. self.reindent_to(effective + self.indentwidth)
  168. else:
  169. if self.usetabs:
  170. pad = '\t'
  171. else:
  172. effective = len(prefix.expandtabs(self.tabwidth))
  173. n = self.indentwidth
  174. pad = ' ' * (n - effective % n)
  175. text.insert("insert", pad)
  176. text.see("insert")
  177. return "break"
  178. finally:
  179. text.undo_block_stop()
  180. def newline_and_indent_event(self, event):
  181. text = self.text
  182. first, last = self.editwin.get_selection_indices()
  183. text.undo_block_start()
  184. try:
  185. if first and last:
  186. text.delete(first, last)
  187. text.mark_set("insert", first)
  188. line = text.get("insert linestart", "insert")
  189. i, n = 0, len(line)
  190. while i < n and line[i] in " \t":
  191. i = i+1
  192. if i == n:
  193. # the cursor is in or at leading indentation; just inject
  194. # an empty line at the start and strip space from current line
  195. text.delete("insert - %d chars" % i, "insert")
  196. text.insert("insert linestart", '\n')
  197. return "break"
  198. indent = line[:i]
  199. # strip whitespace before insert point
  200. i = 0
  201. while line and line[-1] in " \t":
  202. line = line[:-1]
  203. i = i+1
  204. if i:
  205. text.delete("insert - %d chars" % i, "insert")
  206. # strip whitespace after insert point
  207. while text.get("insert") in " \t":
  208. text.delete("insert")
  209. # start new line
  210. text.insert("insert", '\n')
  211. # adjust indentation for continuations and block
  212. # open/close first need to find the last stmt
  213. lno = index2line(text.index('insert'))
  214. y = PyParse.Parser(self.indentwidth, self.tabwidth)
  215. for context in self.num_context_lines:
  216. startat = max(lno - context, 1)
  217. startatindex = repr(startat) + ".0"
  218. rawtext = text.get(startatindex, "insert")
  219. y.set_str(rawtext)
  220. bod = y.find_good_parse_start(
  221. self.context_use_ps1,
  222. self._build_char_in_string_func(startatindex))
  223. if bod is not None or startat == 1:
  224. break
  225. y.set_lo(bod or 0)
  226. c = y.get_continuation_type()
  227. if c != PyParse.C_NONE:
  228. # The current stmt hasn't ended yet.
  229. if c == PyParse.C_STRING:
  230. # inside a string; just mimic the current indent
  231. text.insert("insert", indent)
  232. elif c == PyParse.C_BRACKET:
  233. # line up with the first (if any) element of the
  234. # last open bracket structure; else indent one
  235. # level beyond the indent of the line with the
  236. # last open bracket
  237. self.reindent_to(y.compute_bracket_indent())
  238. elif c == PyParse.C_BACKSLASH:
  239. # if more than one line in this stmt already, just
  240. # mimic the current indent; else if initial line
  241. # has a start on an assignment stmt, indent to
  242. # beyond leftmost =; else to beyond first chunk of
  243. # non-whitespace on initial line
  244. if y.get_num_lines_in_stmt() > 1:
  245. text.insert("insert", indent)
  246. else:
  247. self.reindent_to(y.compute_backslash_indent())
  248. else:
  249. assert 0, "bogus continuation type " + repr(c)
  250. return "break"
  251. # This line starts a brand new stmt; indent relative to
  252. # indentation of initial line of closest preceding
  253. # interesting stmt.
  254. indent = y.get_base_indent_string()
  255. text.insert("insert", indent)
  256. if y.is_block_opener():
  257. self.smart_indent_event(event)
  258. elif indent and y.is_block_closer():
  259. self.smart_backspace_event(event)
  260. return "break"
  261. finally:
  262. text.see("insert")
  263. text.undo_block_stop()
  264. auto_indent = newline_and_indent_event
  265. # Our editwin provides a is_char_in_string function that works
  266. # with a Tk text index, but PyParse only knows about offsets into
  267. # a string. This builds a function for PyParse that accepts an
  268. # offset.
  269. def _build_char_in_string_func(self, startindex):
  270. def inner(offset, _startindex=startindex,
  271. _icis=self.editwin.is_char_in_string):
  272. return _icis(_startindex + "+%dc" % offset)
  273. return inner
  274. def indent_region_event(self, event):
  275. head, tail, chars, lines = self.get_region()
  276. for pos in range(len(lines)):
  277. line = lines[pos]
  278. if line:
  279. raw, effective = classifyws(line, self.tabwidth)
  280. effective = effective + self.indentwidth
  281. lines[pos] = self._make_blanks(effective) + line[raw:]
  282. self.set_region(head, tail, chars, lines)
  283. return "break"
  284. def dedent_region_event(self, event):
  285. head, tail, chars, lines = self.get_region()
  286. for pos in range(len(lines)):
  287. line = lines[pos]
  288. if line:
  289. raw, effective = classifyws(line, self.tabwidth)
  290. effective = max(effective - self.indentwidth, 0)
  291. lines[pos] = self._make_blanks(effective) + line[raw:]
  292. self.set_region(head, tail, chars, lines)
  293. return "break"
  294. def comment_region_event(self, event):
  295. head, tail, chars, lines = self.get_region()
  296. for pos in range(len(lines) - 1):
  297. line = lines[pos]
  298. lines[pos] = '##' + line
  299. self.set_region(head, tail, chars, lines)
  300. def uncomment_region_event(self, event):
  301. head, tail, chars, lines = self.get_region()
  302. for pos in range(len(lines)):
  303. line = lines[pos]
  304. if not line:
  305. continue
  306. if line[:2] == '##':
  307. line = line[2:]
  308. elif line[:1] == '#':
  309. line = line[1:]
  310. lines[pos] = line
  311. self.set_region(head, tail, chars, lines)
  312. def tabify_region_event(self, event):
  313. head, tail, chars, lines = self.get_region()
  314. tabwidth = self._asktabwidth()
  315. for pos in range(len(lines)):
  316. line = lines[pos]
  317. if line:
  318. raw, effective = classifyws(line, tabwidth)
  319. ntabs, nspaces = divmod(effective, tabwidth)
  320. lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
  321. self.set_region(head, tail, chars, lines)
  322. def untabify_region_event(self, event):
  323. head, tail, chars, lines = self.get_region()
  324. tabwidth = self._asktabwidth()
  325. for pos in range(len(lines)):
  326. lines[pos] = lines[pos].expandtabs(tabwidth)
  327. self.set_region(head, tail, chars, lines)
  328. def toggle_tabs_event(self, event):
  329. if self.editwin.askyesno(
  330. "Toggle tabs",
  331. "Turn tabs " + ("on", "off")[self.usetabs] + "?",
  332. parent=self.text):
  333. self.usetabs = not self.usetabs
  334. return "break"
  335. # XXX this isn't bound to anything -- see class tabwidth comments
  336. def change_tabwidth_event(self, event):
  337. new = self._asktabwidth()
  338. if new != self.tabwidth:
  339. self.tabwidth = new
  340. self.set_indentation_params(0, guess=0)
  341. return "break"
  342. def change_indentwidth_event(self, event):
  343. new = self.editwin.askinteger(
  344. "Indent width",
  345. "New indent width (1-16)",
  346. parent=self.text,
  347. initialvalue=self.indentwidth,
  348. minvalue=1,
  349. maxvalue=16)
  350. if new and new != self.indentwidth:
  351. self.indentwidth = new
  352. return "break"
  353. def get_region(self):
  354. text = self.text
  355. first, last = self.editwin.get_selection_indices()
  356. if first and last:
  357. head = text.index(first + " linestart")
  358. tail = text.index(last + "-1c lineend +1c")
  359. else:
  360. head = text.index("insert linestart")
  361. tail = text.index("insert lineend +1c")
  362. chars = text.get(head, tail)
  363. lines = chars.split("\n")
  364. return head, tail, chars, lines
  365. def set_region(self, head, tail, chars, lines):
  366. text = self.text
  367. newchars = "\n".join(lines)
  368. if newchars == chars:
  369. text.bell()
  370. return
  371. text.tag_remove("sel", "1.0", "end")
  372. text.mark_set("insert", head)
  373. text.undo_block_start()
  374. text.delete(head, tail)
  375. text.insert(head, newchars)
  376. text.undo_block_stop()
  377. text.tag_add("sel", head, "insert")
  378. # Make string that displays as n leading blanks.
  379. def _make_blanks(self, n):
  380. if self.usetabs:
  381. ntabs, nspaces = divmod(n, self.tabwidth)
  382. return '\t' * ntabs + ' ' * nspaces
  383. else:
  384. return ' ' * n
  385. # Delete from beginning of line to insert point, then reinsert
  386. # column logical (meaning use tabs if appropriate) spaces.
  387. def reindent_to(self, column):
  388. text = self.text
  389. text.undo_block_start()
  390. if text.compare("insert linestart", "!=", "insert"):
  391. text.delete("insert linestart", "insert")
  392. if column:
  393. text.insert("insert", self._make_blanks(column))
  394. text.undo_block_stop()
  395. def _asktabwidth(self):
  396. return self.editwin.askinteger(
  397. "Tab width",
  398. "Spaces per tab?",
  399. parent=self.text,
  400. initialvalue=self.tabwidth,
  401. minvalue=1,
  402. maxvalue=16) or self.tabwidth
  403. # Guess indentwidth from text content.
  404. # Return guessed indentwidth. This should not be believed unless
  405. # it's in a reasonable range (e.g., it will be 0 if no indented
  406. # blocks are found).
  407. def guess_indent(self):
  408. opener, indented = IndentSearcher(self.text, self.tabwidth).run()
  409. if opener and indented:
  410. raw, indentsmall = classifyws(opener, self.tabwidth)
  411. raw, indentlarge = classifyws(indented, self.tabwidth)
  412. else:
  413. indentsmall = indentlarge = 0
  414. return indentlarge - indentsmall
  415. # "line.col" -> line, as an int
  416. def index2line(index):
  417. return int(float(index))
  418. # Look at the leading whitespace in s.
  419. # Return pair (# of leading ws characters,
  420. # effective # of leading blanks after expanding
  421. # tabs to width tabwidth)
  422. def classifyws(s, tabwidth):
  423. raw = effective = 0
  424. for ch in s:
  425. if ch == ' ':
  426. raw = raw + 1
  427. effective = effective + 1
  428. elif ch == '\t':
  429. raw = raw + 1
  430. effective = (effective // tabwidth + 1) * tabwidth
  431. else:
  432. break
  433. return raw, effective
  434. class IndentSearcher:
  435. # .run() chews over the Text widget, looking for a block opener
  436. # and the stmt following it. Returns a pair,
  437. # (line containing block opener, line containing stmt)
  438. # Either or both may be None.
  439. def __init__(self, text, tabwidth):
  440. self.text = text
  441. self.tabwidth = tabwidth
  442. self.i = self.finished = 0
  443. self.blkopenline = self.indentedline = None
  444. def readline(self):
  445. if self.finished:
  446. val = ""
  447. else:
  448. i = self.i = self.i + 1
  449. mark = repr(i) + ".0"
  450. if self.text.compare(mark, ">=", "end"):
  451. val = ""
  452. else:
  453. val = self.text.get(mark, mark + " lineend+1c")
  454. # hrm - not sure this is correct in py3k - the source code may have
  455. # an encoding declared, but the data will *always* be in
  456. # default_scintilla_encoding - so if anyone looks at the encoding decl
  457. # in the source they will be wrong. I think. Maybe. Or something...
  458. return val.encode(default_scintilla_encoding)
  459. def run(self):
  460. OPENERS=('class', 'def', 'for', 'if', 'try', 'while')
  461. INDENT=tokenize.INDENT
  462. NAME=tokenize.NAME
  463. save_tabsize = tokenize.tabsize
  464. tokenize.tabsize = self.tabwidth
  465. try:
  466. try:
  467. for (typ, token, start, end, line) in token_generator(self.readline):
  468. if typ == NAME and token in OPENERS:
  469. self.blkopenline = line
  470. elif typ == INDENT and self.blkopenline:
  471. self.indentedline = line
  472. break
  473. except (tokenize.TokenError, IndentationError):
  474. # since we cut off the tokenizer early, we can trigger
  475. # spurious errors
  476. pass
  477. finally:
  478. tokenize.tabsize = save_tabsize
  479. return self.blkopenline, self.indentedline