2 #from Tkinter import TclError
6 ###$ event <<newline-and-indent>>
12 ###$ event <<indent-region>>
13 ###$ win <Control-bracketright>
14 ###$ unix <Alt-bracketright>
15 ###$ unix <Control-bracketright>
17 ###$ event <<dedent-region>>
18 ###$ win <Control-bracketleft>
19 ###$ unix <Alt-bracketleft>
20 ###$ unix <Control-bracketleft>
22 ###$ event <<comment-region>>
26 ###$ event <<uncomment-region>>
30 ###$ event <<tabify-region>>
34 ###$ event <<untabify-region>>
45 ('_Indent region', '<<indent-region>>'),
46 ('_Dedent region', '<<dedent-region>>'),
47 ('Comment _out region', '<<comment-region>>'),
48 ('U_ncomment region', '<<uncomment-region>>'),
49 ('Tabify region', '<<tabify-region>>'),
50 ('Untabify region', '<<untabify-region>>'),
51 ('Toggle tabs', '<<toggle-tabs>>'),
52 ('New indent width', '<<change-indentwidth>>'),
57 '<<smart-backspace>>': ['<Key-BackSpace>'],
58 '<<newline-and-indent>>': ['<Key-Return>', '<KP_Enter>'],
59 '<<smart-indent>>': ['<Key-Tab>']
63 '<<indent-region>>': ['<Control-bracketright>'],
64 '<<dedent-region>>': ['<Control-bracketleft>'],
65 '<<comment-region>>': ['<Alt-Key-3>'],
66 '<<uncomment-region>>': ['<Alt-Key-4>'],
67 '<<tabify-region>>': ['<Alt-Key-5>'],
68 '<<untabify-region>>': ['<Alt-Key-6>'],
69 '<<toggle-tabs>>': ['<Alt-Key-t>'],
70 '<<change-indentwidth>>': ['<Alt-Key-u>'],
74 '<<indent-region>>': ['<Alt-bracketright>',
75 '<Meta-bracketright>',
76 '<Control-bracketright>'],
77 '<<dedent-region>>': ['<Alt-bracketleft>',
79 '<Control-bracketleft>'],
80 '<<comment-region>>': ['<Alt-Key-3>', '<Meta-Key-3>'],
81 '<<uncomment-region>>': ['<Alt-Key-4>', '<Meta-Key-4>'],
82 '<<tabify-region>>': ['<Alt-Key-5>', '<Meta-Key-5>'],
83 '<<untabify-region>>': ['<Alt-Key-6>', '<Meta-Key-6>'],
84 '<<toggle-tabs>>': ['<Alt-Key-t>'],
85 '<<change-indentwidth>>': ['<Alt-Key-u>'],
88 # usetabs true -> literal tab characters are used by indent and
89 # dedent cmds, possibly mixed with spaces if
90 # indentwidth is not a multiple of tabwidth
91 # false -> tab characters are converted to spaces by indent
92 # and dedent cmds, and ditto TAB keystrokes
93 # indentwidth is the number of characters per logical indent level.
94 # tabwidth is the display width of a literal tab character.
95 # CAUTION: telling Tk to use anything other than its default
96 # tab setting causes it to use an entirely different tabbing algorithm,
97 # treating tab stops as fixed distances from the left margin.
98 # Nobody expects this, so for now tabwidth should never be changed.
101 tabwidth
= 8 # for IDLE use, must remain 8 until Tk is fixed
103 # If context_use_ps1 is true, parsing searches back for a ps1 line;
104 # else searches for a popular (if, def, ...) Python stmt.
107 # When searching backwards for a reliable place to begin parsing,
108 # first start num_context_lines[0] lines back, then
109 # num_context_lines[1] lines back if that didn't work, and so on.
110 # The last value should be huge (larger than the # of lines in a
112 # Making the initial values larger slows things down more often.
113 num_context_lines
= 50, 500, 5000000
115 def __init__(self
, editwin
):
116 self
.editwin
= editwin
117 self
.text
= editwin
.text
119 def config(self
, **options
):
120 for key
, value
in options
.items():
123 elif key
== 'indentwidth':
124 self
.indentwidth
= value
125 elif key
== 'tabwidth':
126 self
.tabwidth
= value
127 elif key
== 'context_use_ps1':
128 self
.context_use_ps1
= value
130 raise KeyError, "bad option name: %s" % `key`
132 # If ispythonsource and guess are true, guess a good value for
133 # indentwidth based on file content (if possible), and if
134 # indentwidth != tabwidth set usetabs false.
135 # In any case, adjust the Text widget's view of what a tab
138 def set_indentation_params(self
, ispythonsource
, guess
=1):
139 if guess
and ispythonsource
:
140 i
= self
.guess_indent()
143 if self
.indentwidth
!= self
.tabwidth
:
146 self
.editwin
.set_tabwidth(self
.tabwidth
)
148 def smart_backspace_event(self
, event
):
150 first
, last
= self
.editwin
.get_selection_indices()
152 text
.delete(first
, last
)
153 text
.mark_set("insert", first
)
155 # Delete whitespace left, until hitting a real char or closest
156 # preceding virtual tab stop.
157 chars
= text
.get("insert linestart", "insert")
159 if text
.compare("insert", ">", "1.0"):
160 # easy: delete preceding newline
161 text
.delete("insert-1c")
163 text
.bell() # at start of buffer
165 if chars
[-1] not in " \t":
166 # easy: delete preceding real char
167 text
.delete("insert-1c")
169 # Ick. It may require *inserting* spaces if we back up over a
170 # tab character! This is written to be clear, not fast.
171 expand
, tabwidth
= string
.expandtabs
, self
.tabwidth
172 have
= len(expand(chars
, tabwidth
))
174 want
= int((have
- 1) / self
.indentwidth
) * self
.indentwidth
178 ncharsdeleted
= ncharsdeleted
+ 1
179 have
= len(expand(chars
, tabwidth
))
180 if have
<= want
or chars
[-1] not in " \t":
182 text
.undo_block_start()
183 text
.delete("insert-%dc" % ncharsdeleted
, "insert")
185 text
.insert("insert", ' ' * (want
- have
))
186 text
.undo_block_stop()
189 def smart_indent_event(self
, event
):
190 # if intraline selection:
192 # elif multiline selection:
193 # do indent-region & return
196 first
, last
= self
.editwin
.get_selection_indices()
197 text
.undo_block_start()
200 if index2line(first
) != index2line(last
):
201 return self
.indent_region_event(event
)
202 text
.delete(first
, last
)
203 text
.mark_set("insert", first
)
204 prefix
= text
.get("insert linestart", "insert")
205 raw
, effective
= classifyws(prefix
, self
.tabwidth
)
206 if raw
== len(prefix
):
207 # only whitespace to the left
208 self
.reindent_to(effective
+ self
.indentwidth
)
213 effective
= len(string
.expandtabs(prefix
,
216 pad
= ' ' * (n
- effective
% n
)
217 text
.insert("insert", pad
)
221 text
.undo_block_stop()
223 def newline_and_indent_event(self
, event
):
225 first
, last
= self
.editwin
.get_selection_indices()
226 text
.undo_block_start()
229 text
.delete(first
, last
)
230 text
.mark_set("insert", first
)
231 line
= text
.get("insert linestart", "insert")
233 while i
< n
and line
[i
] in " \t":
236 # the cursor is in or at leading indentation; just inject
237 # an empty line at the start
238 text
.insert("insert linestart", '\n')
241 # strip whitespace before insert point
243 while line
and line
[-1] in " \t":
247 text
.delete("insert - %d chars" % i
, "insert")
248 # strip whitespace after insert point
249 while text
.get("insert") in " \t":
250 text
.delete("insert")
252 text
.insert("insert", '\n')
254 # adjust indentation for continuations and block
255 # open/close first need to find the last stmt
256 lno
= index2line(text
.index('insert'))
257 y
= PyParse
.Parser(self
.indentwidth
, self
.tabwidth
)
258 for context
in self
.num_context_lines
:
259 startat
= max(lno
- context
, 1)
260 startatindex
= `startat`
+ ".0"
261 rawtext
= text
.get(startatindex
, "insert")
263 bod
= y
.find_good_parse_start(
264 self
.context_use_ps1
,
265 self
._build
_char
_in
_string
_func
(startatindex
))
266 if bod
is not None or startat
== 1:
269 c
= y
.get_continuation_type()
270 if c
!= PyParse
.C_NONE
:
271 # The current stmt hasn't ended yet.
272 if c
== PyParse
.C_STRING
:
273 # inside a string; just mimic the current indent
274 text
.insert("insert", indent
)
275 elif c
== PyParse
.C_BRACKET
:
276 # line up with the first (if any) element of the
277 # last open bracket structure; else indent one
278 # level beyond the indent of the line with the
280 self
.reindent_to(y
.compute_bracket_indent())
281 elif c
== PyParse
.C_BACKSLASH
:
282 # if more than one line in this stmt already, just
283 # mimic the current indent; else if initial line
284 # has a start on an assignment stmt, indent to
285 # beyond leftmost =; else to beyond first chunk of
286 # non-whitespace on initial line
287 if y
.get_num_lines_in_stmt() > 1:
288 text
.insert("insert", indent
)
290 self
.reindent_to(y
.compute_backslash_indent())
292 assert 0, "bogus continuation type " + `c`
295 # This line starts a brand new stmt; indent relative to
296 # indentation of initial line of closest preceding
298 indent
= y
.get_base_indent_string()
299 text
.insert("insert", indent
)
300 if y
.is_block_opener():
301 self
.smart_indent_event(event
)
302 elif indent
and y
.is_block_closer():
303 self
.smart_backspace_event(event
)
307 text
.undo_block_stop()
309 auto_indent
= newline_and_indent_event
311 # Our editwin provides a is_char_in_string function that works
312 # with a Tk text index, but PyParse only knows about offsets into
313 # a string. This builds a function for PyParse that accepts an
316 def _build_char_in_string_func(self
, startindex
):
317 def inner(offset
, _startindex
=startindex
,
318 _icis
=self
.editwin
.is_char_in_string
):
319 return _icis(_startindex
+ "+%dc" % offset
)
322 def indent_region_event(self
, event
):
323 head
, tail
, chars
, lines
= self
.get_region()
324 for pos
in range(len(lines
)):
327 raw
, effective
= classifyws(line
, self
.tabwidth
)
328 effective
= effective
+ self
.indentwidth
329 lines
[pos
] = self
._make
_blanks
(effective
) + line
[raw
:]
330 self
.set_region(head
, tail
, chars
, lines
)
333 def dedent_region_event(self
, event
):
334 head
, tail
, chars
, lines
= self
.get_region()
335 for pos
in range(len(lines
)):
338 raw
, effective
= classifyws(line
, self
.tabwidth
)
339 effective
= max(effective
- self
.indentwidth
, 0)
340 lines
[pos
] = self
._make
_blanks
(effective
) + line
[raw
:]
341 self
.set_region(head
, tail
, chars
, lines
)
344 def comment_region_event(self
, event
):
345 head
, tail
, chars
, lines
= self
.get_region()
346 for pos
in range(len(lines
) - 1):
348 lines
[pos
] = '##' + line
349 self
.set_region(head
, tail
, chars
, lines
)
351 def uncomment_region_event(self
, event
):
352 head
, tail
, chars
, lines
= self
.get_region()
353 for pos
in range(len(lines
)):
359 elif line
[:1] == '#':
362 self
.set_region(head
, tail
, chars
, lines
)
364 def tabify_region_event(self
, event
):
365 head
, tail
, chars
, lines
= self
.get_region()
366 tabwidth
= self
._asktabwidth
()
367 for pos
in range(len(lines
)):
370 raw
, effective
= classifyws(line
, tabwidth
)
371 ntabs
, nspaces
= divmod(effective
, tabwidth
)
372 lines
[pos
] = '\t' * ntabs
+ ' ' * nspaces
+ line
[raw
:]
373 self
.set_region(head
, tail
, chars
, lines
)
375 def untabify_region_event(self
, event
):
376 head
, tail
, chars
, lines
= self
.get_region()
377 tabwidth
= self
._asktabwidth
()
378 for pos
in range(len(lines
)):
379 lines
[pos
] = string
.expandtabs(lines
[pos
], tabwidth
)
380 self
.set_region(head
, tail
, chars
, lines
)
382 def toggle_tabs_event(self
, event
):
383 if self
.editwin
.askyesno(
385 "Turn tabs " + ("on", "off")[self
.usetabs
] + "?",
387 self
.usetabs
= not self
.usetabs
390 # XXX this isn't bound to anything -- see class tabwidth comments
391 def change_tabwidth_event(self
, event
):
392 new
= self
._asktabwidth
()
393 if new
!= self
.tabwidth
:
395 self
.set_indentation_params(0, guess
=0)
398 def change_indentwidth_event(self
, event
):
399 new
= self
.editwin
.askinteger(
401 "New indent width (1-16)",
403 initialvalue
=self
.indentwidth
,
406 if new
and new
!= self
.indentwidth
:
407 self
.indentwidth
= new
410 def get_region(self
):
412 first
, last
= self
.editwin
.get_selection_indices()
414 head
= text
.index(first
+ " linestart")
415 tail
= text
.index(last
+ "-1c lineend +1c")
417 head
= text
.index("insert linestart")
418 tail
= text
.index("insert lineend +1c")
419 chars
= text
.get(head
, tail
)
420 lines
= string
.split(chars
, "\n")
421 return head
, tail
, chars
, lines
423 def set_region(self
, head
, tail
, chars
, lines
):
425 newchars
= string
.join(lines
, "\n")
426 if newchars
== chars
:
429 text
.tag_remove("sel", "1.0", "end")
430 text
.mark_set("insert", head
)
431 text
.undo_block_start()
432 text
.delete(head
, tail
)
433 text
.insert(head
, newchars
)
434 text
.undo_block_stop()
435 text
.tag_add("sel", head
, "insert")
437 # Make string that displays as n leading blanks.
439 def _make_blanks(self
, n
):
441 ntabs
, nspaces
= divmod(n
, self
.tabwidth
)
442 return '\t' * ntabs
+ ' ' * nspaces
446 # Delete from beginning of line to insert point, then reinsert
447 # column logical (meaning use tabs if appropriate) spaces.
449 def reindent_to(self
, column
):
451 text
.undo_block_start()
452 if text
.compare("insert linestart", "!=", "insert"):
453 text
.delete("insert linestart", "insert")
455 text
.insert("insert", self
._make
_blanks
(column
))
456 text
.undo_block_stop()
458 def _asktabwidth(self
):
459 return self
.editwin
.askinteger(
463 initialvalue
=self
.tabwidth
,
465 maxvalue
=16) or self
.tabwidth
467 # Guess indentwidth from text content.
468 # Return guessed indentwidth. This should not be believed unless
469 # it's in a reasonable range (e.g., it will be 0 if no indented
472 def guess_indent(self
):
473 opener
, indented
= IndentSearcher(self
.text
, self
.tabwidth
).run()
474 if opener
and indented
:
475 raw
, indentsmall
= classifyws(opener
, self
.tabwidth
)
476 raw
, indentlarge
= classifyws(indented
, self
.tabwidth
)
478 indentsmall
= indentlarge
= 0
479 return indentlarge
- indentsmall
481 # "line.col" -> line, as an int
482 def index2line(index
):
483 return int(float(index
))
485 # Look at the leading whitespace in s.
486 # Return pair (# of leading ws characters,
487 # effective # of leading blanks after expanding
488 # tabs to width tabwidth)
490 def classifyws(s
, tabwidth
):
495 effective
= effective
+ 1
498 effective
= (effective
/ tabwidth
+ 1) * tabwidth
501 return raw
, effective
507 class IndentSearcher
:
509 # .run() chews over the Text widget, looking for a block opener
510 # and the stmt following it. Returns a pair,
511 # (line containing block opener, line containing stmt)
512 # Either or both may be None.
514 def __init__(self
, text
, tabwidth
):
516 self
.tabwidth
= tabwidth
517 self
.i
= self
.finished
= 0
518 self
.blkopenline
= self
.indentedline
= None
523 i
= self
.i
= self
.i
+ 1
525 if self
.text
.compare(mark
, ">=", "end"):
527 return self
.text
.get(mark
, mark
+ " lineend+1c")
529 def tokeneater(self
, type, token
, start
, end
, line
,
530 INDENT
=_tokenize
.INDENT
,
532 OPENERS
=('class', 'def', 'for', 'if', 'try', 'while')):
535 elif type == NAME
and token
in OPENERS
:
536 self
.blkopenline
= line
537 elif type == INDENT
and self
.blkopenline
:
538 self
.indentedline
= line
542 save_tabsize
= _tokenize
.tabsize
543 _tokenize
.tabsize
= self
.tabwidth
546 _tokenize
.tokenize(self
.readline
, self
.tokeneater
)
547 except _tokenize
.TokenError
:
548 # since we cut off the tokenizer early, we can trigger
552 _tokenize
.tabsize
= save_tabsize
553 return self
.blkopenline
, self
.indentedline