blob: d48812d23d1da05b8344ac9f1ba9cce08a1086e1 [file] [log] [blame]
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +00001import string
Guido van Rossum504b0bf1999-01-02 21:28:54 +00002from Tkinter import TclError
Guido van Rossumdef2c961999-05-21 04:38:27 +00003import tkMessageBox
4import tkSimpleDialog
5
6# The default tab setting for a Text widget, in average-width characters.
7TK_TABWIDTH_DEFAULT = 8
Guido van Rossum504b0bf1999-01-02 21:28:54 +00008
9###$ event <<newline-and-indent>>
10###$ win <Key-Return>
11###$ win <KP_Enter>
12###$ unix <Key-Return>
13###$ unix <KP_Enter>
14
15###$ event <<indent-region>>
16###$ win <Control-bracketright>
17###$ unix <Alt-bracketright>
18###$ unix <Control-bracketright>
19
20###$ event <<dedent-region>>
21###$ win <Control-bracketleft>
22###$ unix <Alt-bracketleft>
23###$ unix <Control-bracketleft>
24
25###$ event <<comment-region>>
26###$ win <Alt-Key-3>
27###$ unix <Alt-Key-3>
28
29###$ event <<uncomment-region>>
30###$ win <Alt-Key-4>
31###$ unix <Alt-Key-4>
32
33###$ event <<tabify-region>>
34###$ win <Alt-Key-5>
35###$ unix <Alt-Key-5>
36
37###$ event <<untabify-region>>
38###$ win <Alt-Key-6>
39###$ unix <Alt-Key-6>
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +000040
Guido van Rossum17c516e1999-04-19 16:23:15 +000041import re
Guido van Rossum17c516e1999-04-19 16:23:15 +000042_is_block_closer = re.compile(r"""
43 \s*
44 ( return
45 | break
46 | continue
47 | raise
48 | pass
49 )
50 \b
51""", re.VERBOSE).match
Guido van Rossum8234dfc1999-06-01 15:03:30 +000052
53# colon followed by optional comment
54_looks_like_opener = re.compile(r":\s*(#.*)?$").search
Guido van Rossum17c516e1999-04-19 16:23:15 +000055del re
56
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +000057class AutoIndent:
58
Guido van Rossum504b0bf1999-01-02 21:28:54 +000059 menudefs = [
60 ('edit', [
61 None,
62 ('_Indent region', '<<indent-region>>'),
63 ('_Dedent region', '<<dedent-region>>'),
64 ('Comment _out region', '<<comment-region>>'),
65 ('U_ncomment region', '<<uncomment-region>>'),
66 ('Tabify region', '<<tabify-region>>'),
67 ('Untabify region', '<<untabify-region>>'),
Guido van Rossumdef2c961999-05-21 04:38:27 +000068 ('Toggle tabs', '<<toggle-tabs>>'),
69 ('New tab width', '<<change-tabwidth>>'),
70 ('New indent width', '<<change-indentwidth>>'),
Guido van Rossum504b0bf1999-01-02 21:28:54 +000071 ]),
72 ]
73
Guido van Rossum33f2b7b1999-01-03 00:47:35 +000074 keydefs = {
75 '<<smart-backspace>>': ['<Key-BackSpace>'],
Guido van Rossum504b0bf1999-01-02 21:28:54 +000076 '<<newline-and-indent>>': ['<Key-Return>', '<KP_Enter>'],
Guido van Rossum17c516e1999-04-19 16:23:15 +000077 '<<smart-indent>>': ['<Key-Tab>']
Guido van Rossum33f2b7b1999-01-03 00:47:35 +000078 }
79
80 windows_keydefs = {
Guido van Rossum504b0bf1999-01-02 21:28:54 +000081 '<<indent-region>>': ['<Control-bracketright>'],
82 '<<dedent-region>>': ['<Control-bracketleft>'],
83 '<<comment-region>>': ['<Alt-Key-3>'],
84 '<<uncomment-region>>': ['<Alt-Key-4>'],
85 '<<tabify-region>>': ['<Alt-Key-5>'],
86 '<<untabify-region>>': ['<Alt-Key-6>'],
Guido van Rossumdef2c961999-05-21 04:38:27 +000087 '<<toggle-tabs>>': ['<Alt-Key-t>'],
88 '<<change-tabwidth>>': ['<Alt-Key-u>'],
89 '<<change-indentwidth>>': ['<Alt-Key-v>'],
Guido van Rossum504b0bf1999-01-02 21:28:54 +000090 }
91
92 unix_keydefs = {
Guido van Rossum504b0bf1999-01-02 21:28:54 +000093 '<<indent-region>>': ['<Alt-bracketright>',
94 '<Meta-bracketright>',
95 '<Control-bracketright>'],
96 '<<dedent-region>>': ['<Alt-bracketleft>',
97 '<Meta-bracketleft>',
98 '<Control-bracketleft>'],
99 '<<comment-region>>': ['<Alt-Key-3>', '<Meta-Key-3>'],
100 '<<uncomment-region>>': ['<Alt-Key-4>', '<Meta-Key-4>'],
101 '<<tabify-region>>': ['<Alt-Key-5>', '<Meta-Key-5>'],
102 '<<untabify-region>>': ['<Alt-Key-6>', '<Meta-Key-6>'],
103 }
104
Guido van Rossumdef2c961999-05-21 04:38:27 +0000105 # usetabs true -> literal tab characters are used by indent and
106 # dedent cmds, possibly mixed with spaces if
107 # indentwidth is not a multiple of tabwidth
108 # false -> tab characters are converted to spaces by indent
109 # and dedent cmds, and ditto TAB keystrokes
110 # indentwidth is the number of characters per logical indent level
111 # tabwidth is the display width of a literal tab character
112 usetabs = 0
113 indentwidth = 4
114 tabwidth = 8
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000115
116 def __init__(self, editwin):
117 self.text = editwin.text
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000118
119 def config(self, **options):
120 for key, value in options.items():
Guido van Rossumdef2c961999-05-21 04:38:27 +0000121 if key == 'usetabs':
122 self.usetabs = value
123 elif key == 'indentwidth':
124 self.indentwidth = value
125 elif key == 'tabwidth':
126 self.tabwidth = value
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000127 else:
128 raise KeyError, "bad option name: %s" % `key`
129
Guido van Rossumdef2c961999-05-21 04:38:27 +0000130 # If ispythonsource and guess are true, guess a good value for
131 # indentwidth based on file content (if possible), and if
132 # indentwidth != tabwidth set usetabs false.
133 # In any case, adjust the Text widget's view of what a tab
134 # character means.
135
136 def set_indentation_params(self, ispythonsource, guess=1):
137 text = self.text
138
139 if guess and ispythonsource:
140 i = self.guess_indent()
Guido van Rossum8234dfc1999-06-01 15:03:30 +0000141 ##import sys
Guido van Rossumdef2c961999-05-21 04:38:27 +0000142 ##sys.__stdout__.write("indent %d\n" % i)
143 if 2 <= i <= 8:
144 self.indentwidth = i
145 if self.indentwidth != self.tabwidth:
146 self.usetabs = 0
147
148 current_tabs = text['tabs']
149 if current_tabs == "" and self.tabwidth == TK_TABWIDTH_DEFAULT:
150 pass
151 else:
152 # Reconfigure the Text widget by measuring the width
153 # of a tabwidth-length string in pixels, forcing the
154 # widget's tab stops to that.
155 need_tabs = text.tk.call("font", "measure", text['font'],
156 "-displayof", text.master,
157 "n" * self.tabwidth)
158 if current_tabs != need_tabs:
159 text.configure(tabs=need_tabs)
160
Guido van Rossum33f2b7b1999-01-03 00:47:35 +0000161 def smart_backspace_event(self, event):
162 text = self.text
163 try:
164 first = text.index("sel.first")
165 last = text.index("sel.last")
166 except TclError:
167 first = last = None
168 if first and last:
169 text.delete(first, last)
170 text.mark_set("insert", first)
171 return "break"
Guido van Rossumdef2c961999-05-21 04:38:27 +0000172 # If we're at the end of leading whitespace, nuke one indent
173 # level, else one character.
Guido van Rossum33f2b7b1999-01-03 00:47:35 +0000174 chars = text.get("insert linestart", "insert")
Guido van Rossumdef2c961999-05-21 04:38:27 +0000175 raw, effective = classifyws(chars, self.tabwidth)
176 if 0 < raw == len(chars):
177 if effective >= self.indentwidth:
178 self.reindent_to(effective - self.indentwidth)
179 return "break"
180 text.delete("insert-1c")
Guido van Rossum33f2b7b1999-01-03 00:47:35 +0000181 return "break"
182
Guido van Rossum17c516e1999-04-19 16:23:15 +0000183 def smart_indent_event(self, event):
184 # if intraline selection:
185 # delete it
186 # elif multiline selection:
187 # do indent-region & return
Guido van Rossumdef2c961999-05-21 04:38:27 +0000188 # indent one level
Guido van Rossum17c516e1999-04-19 16:23:15 +0000189 text = self.text
190 try:
191 first = text.index("sel.first")
192 last = text.index("sel.last")
193 except TclError:
194 first = last = None
Guido van Rossum318a70d1999-05-03 15:49:52 +0000195 text.undo_block_start()
196 try:
197 if first and last:
198 if index2line(first) != index2line(last):
199 return self.indent_region_event(event)
200 text.delete(first, last)
201 text.mark_set("insert", first)
Guido van Rossumdef2c961999-05-21 04:38:27 +0000202 prefix = text.get("insert linestart", "insert")
203 raw, effective = classifyws(prefix, self.tabwidth)
204 if raw == len(prefix):
205 # only whitespace to the left
206 self.reindent_to(effective + self.indentwidth)
Guido van Rossum318a70d1999-05-03 15:49:52 +0000207 else:
Guido van Rossumdef2c961999-05-21 04:38:27 +0000208 if self.usetabs:
209 pad = '\t'
210 else:
211 effective = len(string.expandtabs(prefix,
212 self.tabwidth))
213 n = self.indentwidth
214 pad = ' ' * (n - effective % n)
215 text.insert("insert", pad)
Guido van Rossum318a70d1999-05-03 15:49:52 +0000216 text.see("insert")
217 return "break"
218 finally:
219 text.undo_block_stop()
Guido van Rossum17c516e1999-04-19 16:23:15 +0000220
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000221 def newline_and_indent_event(self, event):
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000222 text = self.text
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000223 try:
224 first = text.index("sel.first")
225 last = text.index("sel.last")
226 except TclError:
227 first = last = None
Guido van Rossum318a70d1999-05-03 15:49:52 +0000228 text.undo_block_start()
229 try:
230 if first and last:
231 text.delete(first, last)
232 text.mark_set("insert", first)
233 line = text.get("insert linestart", "insert")
234 i, n = 0, len(line)
235 while i < n and line[i] in " \t":
236 i = i+1
237 indent = line[:i]
238 # strip trailing whitespace
239 i = 0
240 while line and line[-1] in " \t":
241 line = line[:-1]
242 i = i + 1
243 if i:
244 text.delete("insert - %d chars" % i, "insert")
Guido van Rossumdef2c961999-05-21 04:38:27 +0000245 # XXX this reproduces the current line's indentation,
246 # without regard for usetabs etc; could instead insert
247 # "\n" + self._make_blanks(classifyws(indent)[1]).
Guido van Rossum318a70d1999-05-03 15:49:52 +0000248 text.insert("insert", "\n" + indent)
249 if _is_block_opener(line):
250 self.smart_indent_event(event)
Guido van Rossumdef2c961999-05-21 04:38:27 +0000251 elif indent and _is_block_closer(line) and line[-1] != "\\":
Guido van Rossum318a70d1999-05-03 15:49:52 +0000252 self.smart_backspace_event(event)
253 text.see("insert")
254 return "break"
255 finally:
256 text.undo_block_stop()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000257
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000258 auto_indent = newline_and_indent_event
259
260 def indent_region_event(self, event):
261 head, tail, chars, lines = self.get_region()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000262 for pos in range(len(lines)):
263 line = lines[pos]
264 if line:
Guido van Rossumdef2c961999-05-21 04:38:27 +0000265 raw, effective = classifyws(line, self.tabwidth)
266 effective = effective + self.indentwidth
267 lines[pos] = self._make_blanks(effective) + line[raw:]
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000268 self.set_region(head, tail, chars, lines)
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000269 return "break"
270
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000271 def dedent_region_event(self, event):
272 head, tail, chars, lines = self.get_region()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000273 for pos in range(len(lines)):
274 line = lines[pos]
275 if line:
Guido van Rossumdef2c961999-05-21 04:38:27 +0000276 raw, effective = classifyws(line, self.tabwidth)
277 effective = max(effective - self.indentwidth, 0)
278 lines[pos] = self._make_blanks(effective) + line[raw:]
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000279 self.set_region(head, tail, chars, lines)
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000280 return "break"
281
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000282 def comment_region_event(self, event):
283 head, tail, chars, lines = self.get_region()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000284 for pos in range(len(lines)):
285 line = lines[pos]
Guido van Rossumdef2c961999-05-21 04:38:27 +0000286 if line:
287 lines[pos] = '##' + line
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000288 self.set_region(head, tail, chars, lines)
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000289
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000290 def uncomment_region_event(self, event):
291 head, tail, chars, lines = self.get_region()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000292 for pos in range(len(lines)):
293 line = lines[pos]
294 if not line:
295 continue
296 if line[:2] == '##':
297 line = line[2:]
298 elif line[:1] == '#':
299 line = line[1:]
300 lines[pos] = line
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000301 self.set_region(head, tail, chars, lines)
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000302
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000303 def tabify_region_event(self, event):
304 head, tail, chars, lines = self.get_region()
Guido van Rossumdef2c961999-05-21 04:38:27 +0000305 for pos in range(len(lines)):
306 line = lines[pos]
307 if line:
308 raw, effective = classifyws(line, self.tabwidth)
309 ntabs, nspaces = divmod(effective, self.tabwidth)
310 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000311 self.set_region(head, tail, chars, lines)
312
313 def untabify_region_event(self, event):
314 head, tail, chars, lines = self.get_region()
Guido van Rossumdef2c961999-05-21 04:38:27 +0000315 for pos in range(len(lines)):
316 lines[pos] = string.expandtabs(lines[pos], self.tabwidth)
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000317 self.set_region(head, tail, chars, lines)
318
Guido van Rossumdef2c961999-05-21 04:38:27 +0000319 def toggle_tabs_event(self, event):
320 if tkMessageBox.askyesno("Toggle tabs",
321 "Turn tabs " + ("on", "off")[self.usetabs] + "?",
322 parent=self.text):
323 self.usetabs = not self.usetabs
324 return "break"
325
326 def change_tabwidth_event(self, event):
327 new = tkSimpleDialog.askinteger("Tab width",
328 "New tab width (2-16)",
329 parent=self.text,
330 initialvalue=self.tabwidth,
331 minvalue=2, maxvalue=16)
332 if new and new != self.tabwidth:
333 self.tabwidth = new
334 self.set_indentation_params(0, guess=0)
335 return "break"
336
337 def change_indentwidth_event(self, event):
338 new = tkSimpleDialog.askinteger("Indent width",
339 "New indent width (1-16)",
340 parent=self.text,
341 initialvalue=self.indentwidth,
342 minvalue=1, maxvalue=16)
343 if new and new != self.indentwidth:
344 self.indentwidth = new
345 return "break"
346
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000347 def get_region(self):
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000348 text = self.text
349 head = text.index("sel.first linestart")
350 tail = text.index("sel.last -1c lineend +1c")
351 if not (head and tail):
352 head = text.index("insert linestart")
353 tail = text.index("insert lineend +1c")
354 chars = text.get(head, tail)
355 lines = string.split(chars, "\n")
356 return head, tail, chars, lines
357
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000358 def set_region(self, head, tail, chars, lines):
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000359 text = self.text
360 newchars = string.join(lines, "\n")
361 if newchars == chars:
362 text.bell()
363 return
364 text.tag_remove("sel", "1.0", "end")
365 text.mark_set("insert", head)
Guido van Rossum318a70d1999-05-03 15:49:52 +0000366 text.undo_block_start()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000367 text.delete(head, tail)
368 text.insert(head, newchars)
Guido van Rossum318a70d1999-05-03 15:49:52 +0000369 text.undo_block_stop()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000370 text.tag_add("sel", head, "insert")
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000371
Guido van Rossumdef2c961999-05-21 04:38:27 +0000372 # Make string that displays as n leading blanks.
373
374 def _make_blanks(self, n):
375 if self.usetabs:
376 ntabs, nspaces = divmod(n, self.tabwidth)
377 return '\t' * ntabs + ' ' * nspaces
378 else:
379 return ' ' * n
380
381 # Delete from beginning of line to insert point, then reinsert
382 # column logical (meaning use tabs if appropriate) spaces.
383
384 def reindent_to(self, column):
385 text = self.text
386 text.undo_block_start()
387 text.delete("insert linestart", "insert")
388 if column:
389 text.insert("insert", self._make_blanks(column))
390 text.undo_block_stop()
391
392 # Guess indentwidth from text content.
393 # Return guessed indentwidth. This should not be believed unless
394 # it's in a reasonable range (e.g., it will be 0 if no indented
395 # blocks are found).
396
397 def guess_indent(self):
398 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
399 if opener and indented:
400 raw, indentsmall = classifyws(opener, self.tabwidth)
401 raw, indentlarge = classifyws(indented, self.tabwidth)
402 else:
403 indentsmall = indentlarge = 0
404 return indentlarge - indentsmall
Guido van Rossum17c516e1999-04-19 16:23:15 +0000405
406# "line.col" -> line, as an int
407def index2line(index):
408 return int(float(index))
Guido van Rossumdef2c961999-05-21 04:38:27 +0000409
410# Look at the leading whitespace in s.
411# Return pair (# of leading ws characters,
412# effective # of leading blanks after expanding
413# tabs to width tabwidth)
414
415def classifyws(s, tabwidth):
416 raw = effective = 0
417 for ch in s:
418 if ch == ' ':
419 raw = raw + 1
420 effective = effective + 1
421 elif ch == '\t':
422 raw = raw + 1
423 effective = (effective / tabwidth + 1) * tabwidth
424 else:
425 break
426 return raw, effective
427
Guido van Rossum8234dfc1999-06-01 15:03:30 +0000428# Return true iff line probably opens a block. This is a limited
429# analysis based on whether the line's last "interesting" character
430# is a colon.
431
432def _is_block_opener(line):
433 if not _looks_like_opener(line):
434 return 0
435 # Looks like an opener, but possible we're in a comment
436 # x = 3 # and then:
437 # or a string
438 # x = ":#"
439 # If no comment character, we're not in a comment <duh>, and the
440 # colon is the last non-ws char on the line so it's not in a
441 # (single-line) string either.
442 if string.find(line, '#') < 0:
443 return 1
444 # Now it's hard: There's a colon and a comment char. Brute force
445 # approximation.
446 lastch, i, n = 0, 0, len(line)
447 while i < n:
448 ch = line[i]
449 if ch == '\\':
450 lastch = ch
451 i = i+2
452 elif ch in "\"'":
453 # consume string
454 w = 1 # width of string quote
455 if line[i:i+3] in ('"""', "'''"):
456 w = 3
457 ch = ch * 3
458 i = i+w
459 while i < n:
460 if line[i] == '\\':
461 i = i+2
462 elif line[i:i+w] == ch:
463 i = i+w
464 break
465 else:
466 i = i+1
467 lastch = ch
468 elif ch == '#':
469 break
470 else:
471 if ch not in string.whitespace:
472 lastch = ch
473 i = i+1
474 return lastch == ':'
475
Guido van Rossumdef2c961999-05-21 04:38:27 +0000476import tokenize
477_tokenize = tokenize
478del tokenize
479
480class IndentSearcher:
481
482 # .run() chews over the Text widget, looking for a block opener
483 # and the stmt following it. Returns a pair,
484 # (line containing block opener, line containing stmt)
485 # Either or both may be None.
486
487 def __init__(self, text, tabwidth):
488 self.text = text
489 self.tabwidth = tabwidth
490 self.i = self.finished = 0
491 self.blkopenline = self.indentedline = None
492
493 def readline(self):
494 if self.finished:
495 return ""
496 i = self.i = self.i + 1
497 mark = `i` + ".0"
498 if self.text.compare(mark, ">=", "end"):
499 return ""
500 return self.text.get(mark, mark + " lineend+1c")
501
502 def tokeneater(self, type, token, start, end, line,
503 INDENT=_tokenize.INDENT,
504 NAME=_tokenize.NAME,
505 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
506 if self.finished:
507 pass
508 elif type == NAME and token in OPENERS:
509 self.blkopenline = line
510 elif type == INDENT and self.blkopenline:
511 self.indentedline = line
512 self.finished = 1
513
514 def run(self):
515 save_tabsize = _tokenize.tabsize
516 _tokenize.tabsize = self.tabwidth
517 try:
518 try:
519 _tokenize.tokenize(self.readline, self.tokeneater)
520 except _tokenize.TokenError:
521 # since we cut off the tokenizer early, we can trigger
522 # spurious errors
523 pass
524 finally:
525 _tokenize.tabsize = save_tabsize
526 return self.blkopenline, self.indentedline