blob: fa72eb0fb8dacf9ffed4892d57d8637f2ca63512 [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
42_is_block_opener = re.compile(r":\s*(#.*)?$").search
43_is_block_closer = re.compile(r"""
44 \s*
45 ( return
46 | break
47 | continue
48 | raise
49 | pass
50 )
51 \b
52""", re.VERBOSE).match
53del re
54
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +000055class AutoIndent:
56
Guido van Rossum504b0bf1999-01-02 21:28:54 +000057 menudefs = [
58 ('edit', [
59 None,
60 ('_Indent region', '<<indent-region>>'),
61 ('_Dedent region', '<<dedent-region>>'),
62 ('Comment _out region', '<<comment-region>>'),
63 ('U_ncomment region', '<<uncomment-region>>'),
64 ('Tabify region', '<<tabify-region>>'),
65 ('Untabify region', '<<untabify-region>>'),
Guido van Rossumdef2c961999-05-21 04:38:27 +000066 ('Toggle tabs', '<<toggle-tabs>>'),
67 ('New tab width', '<<change-tabwidth>>'),
68 ('New indent width', '<<change-indentwidth>>'),
Guido van Rossum504b0bf1999-01-02 21:28:54 +000069 ]),
70 ]
71
Guido van Rossum33f2b7b1999-01-03 00:47:35 +000072 keydefs = {
73 '<<smart-backspace>>': ['<Key-BackSpace>'],
Guido van Rossum504b0bf1999-01-02 21:28:54 +000074 '<<newline-and-indent>>': ['<Key-Return>', '<KP_Enter>'],
Guido van Rossum17c516e1999-04-19 16:23:15 +000075 '<<smart-indent>>': ['<Key-Tab>']
Guido van Rossum33f2b7b1999-01-03 00:47:35 +000076 }
77
78 windows_keydefs = {
Guido van Rossum504b0bf1999-01-02 21:28:54 +000079 '<<indent-region>>': ['<Control-bracketright>'],
80 '<<dedent-region>>': ['<Control-bracketleft>'],
81 '<<comment-region>>': ['<Alt-Key-3>'],
82 '<<uncomment-region>>': ['<Alt-Key-4>'],
83 '<<tabify-region>>': ['<Alt-Key-5>'],
84 '<<untabify-region>>': ['<Alt-Key-6>'],
Guido van Rossumdef2c961999-05-21 04:38:27 +000085 '<<toggle-tabs>>': ['<Alt-Key-t>'],
86 '<<change-tabwidth>>': ['<Alt-Key-u>'],
87 '<<change-indentwidth>>': ['<Alt-Key-v>'],
Guido van Rossum504b0bf1999-01-02 21:28:54 +000088 }
89
90 unix_keydefs = {
Guido van Rossum504b0bf1999-01-02 21:28:54 +000091 '<<indent-region>>': ['<Alt-bracketright>',
92 '<Meta-bracketright>',
93 '<Control-bracketright>'],
94 '<<dedent-region>>': ['<Alt-bracketleft>',
95 '<Meta-bracketleft>',
96 '<Control-bracketleft>'],
97 '<<comment-region>>': ['<Alt-Key-3>', '<Meta-Key-3>'],
98 '<<uncomment-region>>': ['<Alt-Key-4>', '<Meta-Key-4>'],
99 '<<tabify-region>>': ['<Alt-Key-5>', '<Meta-Key-5>'],
100 '<<untabify-region>>': ['<Alt-Key-6>', '<Meta-Key-6>'],
101 }
102
Guido van Rossumdef2c961999-05-21 04:38:27 +0000103 # usetabs true -> literal tab characters are used by indent and
104 # dedent cmds, possibly mixed with spaces if
105 # indentwidth is not a multiple of tabwidth
106 # false -> tab characters are converted to spaces by indent
107 # and dedent cmds, and ditto TAB keystrokes
108 # indentwidth is the number of characters per logical indent level
109 # tabwidth is the display width of a literal tab character
110 usetabs = 0
111 indentwidth = 4
112 tabwidth = 8
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000113
114 def __init__(self, editwin):
115 self.text = editwin.text
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000116
117 def config(self, **options):
118 for key, value in options.items():
Guido van Rossumdef2c961999-05-21 04:38:27 +0000119 if key == 'usetabs':
120 self.usetabs = value
121 elif key == 'indentwidth':
122 self.indentwidth = value
123 elif key == 'tabwidth':
124 self.tabwidth = value
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000125 else:
126 raise KeyError, "bad option name: %s" % `key`
127
Guido van Rossumdef2c961999-05-21 04:38:27 +0000128 # If ispythonsource and guess are true, guess a good value for
129 # indentwidth based on file content (if possible), and if
130 # indentwidth != tabwidth set usetabs false.
131 # In any case, adjust the Text widget's view of what a tab
132 # character means.
133
134 def set_indentation_params(self, ispythonsource, guess=1):
135 text = self.text
136
137 if guess and ispythonsource:
138 i = self.guess_indent()
139 import sys
140 ##sys.__stdout__.write("indent %d\n" % i)
141 if 2 <= i <= 8:
142 self.indentwidth = i
143 if self.indentwidth != self.tabwidth:
144 self.usetabs = 0
145
146 current_tabs = text['tabs']
147 if current_tabs == "" and self.tabwidth == TK_TABWIDTH_DEFAULT:
148 pass
149 else:
150 # Reconfigure the Text widget by measuring the width
151 # of a tabwidth-length string in pixels, forcing the
152 # widget's tab stops to that.
153 need_tabs = text.tk.call("font", "measure", text['font'],
154 "-displayof", text.master,
155 "n" * self.tabwidth)
156 if current_tabs != need_tabs:
157 text.configure(tabs=need_tabs)
158
Guido van Rossum33f2b7b1999-01-03 00:47:35 +0000159 def smart_backspace_event(self, event):
160 text = self.text
161 try:
162 first = text.index("sel.first")
163 last = text.index("sel.last")
164 except TclError:
165 first = last = None
166 if first and last:
167 text.delete(first, last)
168 text.mark_set("insert", first)
169 return "break"
Guido van Rossumdef2c961999-05-21 04:38:27 +0000170 # If we're at the end of leading whitespace, nuke one indent
171 # level, else one character.
Guido van Rossum33f2b7b1999-01-03 00:47:35 +0000172 chars = text.get("insert linestart", "insert")
Guido van Rossumdef2c961999-05-21 04:38:27 +0000173 raw, effective = classifyws(chars, self.tabwidth)
174 if 0 < raw == len(chars):
175 if effective >= self.indentwidth:
176 self.reindent_to(effective - self.indentwidth)
177 return "break"
178 text.delete("insert-1c")
Guido van Rossum33f2b7b1999-01-03 00:47:35 +0000179 return "break"
180
Guido van Rossum17c516e1999-04-19 16:23:15 +0000181 def smart_indent_event(self, event):
182 # if intraline selection:
183 # delete it
184 # elif multiline selection:
185 # do indent-region & return
Guido van Rossumdef2c961999-05-21 04:38:27 +0000186 # indent one level
Guido van Rossum17c516e1999-04-19 16:23:15 +0000187 text = self.text
188 try:
189 first = text.index("sel.first")
190 last = text.index("sel.last")
191 except TclError:
192 first = last = None
Guido van Rossum318a70d1999-05-03 15:49:52 +0000193 text.undo_block_start()
194 try:
195 if first and last:
196 if index2line(first) != index2line(last):
197 return self.indent_region_event(event)
198 text.delete(first, last)
199 text.mark_set("insert", first)
Guido van Rossumdef2c961999-05-21 04:38:27 +0000200 prefix = text.get("insert linestart", "insert")
201 raw, effective = classifyws(prefix, self.tabwidth)
202 if raw == len(prefix):
203 # only whitespace to the left
204 self.reindent_to(effective + self.indentwidth)
Guido van Rossum318a70d1999-05-03 15:49:52 +0000205 else:
Guido van Rossumdef2c961999-05-21 04:38:27 +0000206 if self.usetabs:
207 pad = '\t'
208 else:
209 effective = len(string.expandtabs(prefix,
210 self.tabwidth))
211 n = self.indentwidth
212 pad = ' ' * (n - effective % n)
213 text.insert("insert", pad)
Guido van Rossum318a70d1999-05-03 15:49:52 +0000214 text.see("insert")
215 return "break"
216 finally:
217 text.undo_block_stop()
Guido van Rossum17c516e1999-04-19 16:23:15 +0000218
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000219 def newline_and_indent_event(self, event):
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000220 text = self.text
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000221 try:
222 first = text.index("sel.first")
223 last = text.index("sel.last")
224 except TclError:
225 first = last = None
Guido van Rossum318a70d1999-05-03 15:49:52 +0000226 text.undo_block_start()
227 try:
228 if first and last:
229 text.delete(first, last)
230 text.mark_set("insert", first)
231 line = text.get("insert linestart", "insert")
232 i, n = 0, len(line)
233 while i < n and line[i] in " \t":
234 i = i+1
235 indent = line[:i]
236 # strip trailing whitespace
237 i = 0
238 while line and line[-1] in " \t":
239 line = line[:-1]
240 i = i + 1
241 if i:
242 text.delete("insert - %d chars" % i, "insert")
Guido van Rossumdef2c961999-05-21 04:38:27 +0000243 # XXX this reproduces the current line's indentation,
244 # without regard for usetabs etc; could instead insert
245 # "\n" + self._make_blanks(classifyws(indent)[1]).
Guido van Rossum318a70d1999-05-03 15:49:52 +0000246 text.insert("insert", "\n" + indent)
247 if _is_block_opener(line):
248 self.smart_indent_event(event)
Guido van Rossumdef2c961999-05-21 04:38:27 +0000249 elif indent and _is_block_closer(line) and line[-1] != "\\":
Guido van Rossum318a70d1999-05-03 15:49:52 +0000250 self.smart_backspace_event(event)
251 text.see("insert")
252 return "break"
253 finally:
254 text.undo_block_stop()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000255
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000256 auto_indent = newline_and_indent_event
257
258 def indent_region_event(self, event):
259 head, tail, chars, lines = self.get_region()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000260 for pos in range(len(lines)):
261 line = lines[pos]
262 if line:
Guido van Rossumdef2c961999-05-21 04:38:27 +0000263 raw, effective = classifyws(line, self.tabwidth)
264 effective = effective + self.indentwidth
265 lines[pos] = self._make_blanks(effective) + line[raw:]
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000266 self.set_region(head, tail, chars, lines)
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000267 return "break"
268
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000269 def dedent_region_event(self, event):
270 head, tail, chars, lines = self.get_region()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000271 for pos in range(len(lines)):
272 line = lines[pos]
273 if line:
Guido van Rossumdef2c961999-05-21 04:38:27 +0000274 raw, effective = classifyws(line, self.tabwidth)
275 effective = max(effective - self.indentwidth, 0)
276 lines[pos] = self._make_blanks(effective) + line[raw:]
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000277 self.set_region(head, tail, chars, lines)
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000278 return "break"
279
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000280 def comment_region_event(self, event):
281 head, tail, chars, lines = self.get_region()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000282 for pos in range(len(lines)):
283 line = lines[pos]
Guido van Rossumdef2c961999-05-21 04:38:27 +0000284 if line:
285 lines[pos] = '##' + line
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000286 self.set_region(head, tail, chars, lines)
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000287
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000288 def uncomment_region_event(self, event):
289 head, tail, chars, lines = self.get_region()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000290 for pos in range(len(lines)):
291 line = lines[pos]
292 if not line:
293 continue
294 if line[:2] == '##':
295 line = line[2:]
296 elif line[:1] == '#':
297 line = line[1:]
298 lines[pos] = line
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000299 self.set_region(head, tail, chars, lines)
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000300
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000301 def tabify_region_event(self, event):
302 head, tail, chars, lines = self.get_region()
Guido van Rossumdef2c961999-05-21 04:38:27 +0000303 for pos in range(len(lines)):
304 line = lines[pos]
305 if line:
306 raw, effective = classifyws(line, self.tabwidth)
307 ntabs, nspaces = divmod(effective, self.tabwidth)
308 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000309 self.set_region(head, tail, chars, lines)
310
311 def untabify_region_event(self, event):
312 head, tail, chars, lines = self.get_region()
Guido van Rossumdef2c961999-05-21 04:38:27 +0000313 for pos in range(len(lines)):
314 lines[pos] = string.expandtabs(lines[pos], self.tabwidth)
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000315 self.set_region(head, tail, chars, lines)
316
Guido van Rossumdef2c961999-05-21 04:38:27 +0000317 def toggle_tabs_event(self, event):
318 if tkMessageBox.askyesno("Toggle tabs",
319 "Turn tabs " + ("on", "off")[self.usetabs] + "?",
320 parent=self.text):
321 self.usetabs = not self.usetabs
322 return "break"
323
324 def change_tabwidth_event(self, event):
325 new = tkSimpleDialog.askinteger("Tab width",
326 "New tab width (2-16)",
327 parent=self.text,
328 initialvalue=self.tabwidth,
329 minvalue=2, maxvalue=16)
330 if new and new != self.tabwidth:
331 self.tabwidth = new
332 self.set_indentation_params(0, guess=0)
333 return "break"
334
335 def change_indentwidth_event(self, event):
336 new = tkSimpleDialog.askinteger("Indent width",
337 "New indent width (1-16)",
338 parent=self.text,
339 initialvalue=self.indentwidth,
340 minvalue=1, maxvalue=16)
341 if new and new != self.indentwidth:
342 self.indentwidth = new
343 return "break"
344
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000345 def get_region(self):
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000346 text = self.text
347 head = text.index("sel.first linestart")
348 tail = text.index("sel.last -1c lineend +1c")
349 if not (head and tail):
350 head = text.index("insert linestart")
351 tail = text.index("insert lineend +1c")
352 chars = text.get(head, tail)
353 lines = string.split(chars, "\n")
354 return head, tail, chars, lines
355
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000356 def set_region(self, head, tail, chars, lines):
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000357 text = self.text
358 newchars = string.join(lines, "\n")
359 if newchars == chars:
360 text.bell()
361 return
362 text.tag_remove("sel", "1.0", "end")
363 text.mark_set("insert", head)
Guido van Rossum318a70d1999-05-03 15:49:52 +0000364 text.undo_block_start()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000365 text.delete(head, tail)
366 text.insert(head, newchars)
Guido van Rossum318a70d1999-05-03 15:49:52 +0000367 text.undo_block_stop()
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000368 text.tag_add("sel", head, "insert")
Guido van Rossum504b0bf1999-01-02 21:28:54 +0000369
Guido van Rossumdef2c961999-05-21 04:38:27 +0000370 # Make string that displays as n leading blanks.
371
372 def _make_blanks(self, n):
373 if self.usetabs:
374 ntabs, nspaces = divmod(n, self.tabwidth)
375 return '\t' * ntabs + ' ' * nspaces
376 else:
377 return ' ' * n
378
379 # Delete from beginning of line to insert point, then reinsert
380 # column logical (meaning use tabs if appropriate) spaces.
381
382 def reindent_to(self, column):
383 text = self.text
384 text.undo_block_start()
385 text.delete("insert linestart", "insert")
386 if column:
387 text.insert("insert", self._make_blanks(column))
388 text.undo_block_stop()
389
390 # Guess indentwidth from text content.
391 # Return guessed indentwidth. This should not be believed unless
392 # it's in a reasonable range (e.g., it will be 0 if no indented
393 # blocks are found).
394
395 def guess_indent(self):
396 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
397 if opener and indented:
398 raw, indentsmall = classifyws(opener, self.tabwidth)
399 raw, indentlarge = classifyws(indented, self.tabwidth)
400 else:
401 indentsmall = indentlarge = 0
402 return indentlarge - indentsmall
Guido van Rossum17c516e1999-04-19 16:23:15 +0000403
404# "line.col" -> line, as an int
405def index2line(index):
406 return int(float(index))
Guido van Rossumdef2c961999-05-21 04:38:27 +0000407
408# Look at the leading whitespace in s.
409# Return pair (# of leading ws characters,
410# effective # of leading blanks after expanding
411# tabs to width tabwidth)
412
413def classifyws(s, tabwidth):
414 raw = effective = 0
415 for ch in s:
416 if ch == ' ':
417 raw = raw + 1
418 effective = effective + 1
419 elif ch == '\t':
420 raw = raw + 1
421 effective = (effective / tabwidth + 1) * tabwidth
422 else:
423 break
424 return raw, effective
425
426import tokenize
427_tokenize = tokenize
428del tokenize
429
430class IndentSearcher:
431
432 # .run() chews over the Text widget, looking for a block opener
433 # and the stmt following it. Returns a pair,
434 # (line containing block opener, line containing stmt)
435 # Either or both may be None.
436
437 def __init__(self, text, tabwidth):
438 self.text = text
439 self.tabwidth = tabwidth
440 self.i = self.finished = 0
441 self.blkopenline = self.indentedline = None
442
443 def readline(self):
444 if self.finished:
445 return ""
446 i = self.i = self.i + 1
447 mark = `i` + ".0"
448 if self.text.compare(mark, ">=", "end"):
449 return ""
450 return self.text.get(mark, mark + " lineend+1c")
451
452 def tokeneater(self, type, token, start, end, line,
453 INDENT=_tokenize.INDENT,
454 NAME=_tokenize.NAME,
455 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
456 if self.finished:
457 pass
458 elif type == NAME and token in OPENERS:
459 self.blkopenline = line
460 elif type == INDENT and self.blkopenline:
461 self.indentedline = line
462 self.finished = 1
463
464 def run(self):
465 save_tabsize = _tokenize.tabsize
466 _tokenize.tabsize = self.tabwidth
467 try:
468 try:
469 _tokenize.tokenize(self.readline, self.tokeneater)
470 except _tokenize.TokenError:
471 # since we cut off the tokenizer early, we can trigger
472 # spurious errors
473 pass
474 finally:
475 _tokenize.tabsize = save_tabsize
476 return self.blkopenline, self.indentedline