1 #from Tkinter import TclError
5 ###$ event <<newline-and-indent>>
11 ###$ event <<indent-region>>
12 ###$ win <Control-bracketright>
13 ###$ unix <Alt-bracketright>
14 ###$ unix <Control-bracketright>
16 ###$ event <<dedent-region>>
17 ###$ win <Control-bracketleft>
18 ###$ unix <Alt-bracketleft>
19 ###$ unix <Control-bracketleft>
21 ###$ event <<comment-region>>
25 ###$ event <<uncomment-region>>
29 ###$ event <<tabify-region>>
33 ###$ event <<untabify-region>>
44 ('_Indent region', '<<indent-region>>'),
45 ('_Dedent region', '<<dedent-region>>'),
46 ('Comment _out region', '<<comment-region>>'),
47 ('U_ncomment region', '<<uncomment-region>>'),
48 ('Tabify region', '<<tabify-region>>'),
49 ('Untabify region', '<<untabify-region>>'),
50 ('Toggle tabs', '<<toggle-tabs>>'),
51 ('New indent width', '<<change-indentwidth>>'),
56 '<<smart-backspace>>': ['<Key-BackSpace>'],
57 '<<newline-and-indent>>': ['<Key-Return>', '<KP_Enter>'],
58 '<<smart-indent>>': ['<Key-Tab>']
62 '<<indent-region>>': ['<Control-bracketright>'],
63 '<<dedent-region>>': ['<Control-bracketleft>'],
64 '<<comment-region>>': ['<Alt-Key-3>'],
65 '<<uncomment-region>>': ['<Alt-Key-4>'],
66 '<<tabify-region>>': ['<Alt-Key-5>'],
67 '<<untabify-region>>': ['<Alt-Key-6>'],
68 '<<toggle-tabs>>': ['<Alt-Key-t>'],
69 '<<change-indentwidth>>': ['<Alt-Key-u>'],
73 '<<indent-region>>': ['<Alt-bracketright>',
74 '<Meta-bracketright>',
75 '<Control-bracketright>'],
76 '<<dedent-region>>': ['<Alt-bracketleft>',
78 '<Control-bracketleft>'],
79 '<<comment-region>>': ['<Alt-Key-3>', '<Meta-Key-3>'],
80 '<<uncomment-region>>': ['<Alt-Key-4>', '<Meta-Key-4>'],
81 '<<tabify-region>>': ['<Alt-Key-5>', '<Meta-Key-5>'],
82 '<<untabify-region>>': ['<Alt-Key-6>', '<Meta-Key-6>'],
83 '<<toggle-tabs>>': ['<Alt-Key-t>'],
84 '<<change-indentwidth>>': ['<Alt-Key-u>'],
87 # usetabs true -> literal tab characters are used by indent and
88 # dedent cmds, possibly mixed with spaces if
89 # indentwidth is not a multiple of tabwidth
90 # false -> tab characters are converted to spaces by indent
91 # and dedent cmds, and ditto TAB keystrokes
92 # indentwidth is the number of characters per logical indent level.
93 # tabwidth is the display width of a literal tab character.
94 # CAUTION: telling Tk to use anything other than its default
95 # tab setting causes it to use an entirely different tabbing algorithm,
96 # treating tab stops as fixed distances from the left margin.
97 # Nobody expects this, so for now tabwidth should never be changed.
100 tabwidth
= 8 # for IDLE use, must remain 8 until Tk is fixed
102 # If context_use_ps1 is true, parsing searches back for a ps1 line;
103 # else searches for a popular (if, def, ...) Python stmt.
106 # When searching backwards for a reliable place to begin parsing,
107 # first start num_context_lines[0] lines back, then
108 # num_context_lines[1] lines back if that didn't work, and so on.
109 # The last value should be huge (larger than the # of lines in a
111 # Making the initial values larger slows things down more often.
112 num_context_lines
= 50, 500, 5000000
114 def __init__(self
, editwin
):
115 self
.editwin
= editwin
116 self
.text
= editwin
.text
118 def config(self
, **options
):
119 for key
, value
in options
.items():
122 elif key
== 'indentwidth':
123 self
.indentwidth
= value
124 elif key
== 'tabwidth':
125 self
.tabwidth
= value
126 elif key
== 'context_use_ps1':
127 self
.context_use_ps1
= value
129 raise KeyError, "bad option name: %s" % `key`
131 # If ispythonsource and guess are true, guess a good value for
132 # indentwidth based on file content (if possible), and if
133 # indentwidth != tabwidth set usetabs false.
134 # In any case, adjust the Text widget's view of what a tab
137 def set_indentation_params(self
, ispythonsource
, guess
=1):
138 if guess
and ispythonsource
:
139 i
= self
.guess_indent()
142 if self
.indentwidth
!= self
.tabwidth
:
145 self
.editwin
.set_tabwidth(self
.tabwidth
)
147 def smart_backspace_event(self
, event
):
149 first
, last
= self
.editwin
.get_selection_indices()
151 text
.delete(first
, last
)
152 text
.mark_set("insert", first
)
154 # Delete whitespace left, until hitting a real char or closest
155 # preceding virtual tab stop.
156 chars
= text
.get("insert linestart", "insert")
158 if text
.compare("insert", ">", "1.0"):
159 # easy: delete preceding newline
160 text
.delete("insert-1c")
162 text
.bell() # at start of buffer
164 if chars
[-1] not in " \t":
165 # easy: delete preceding real char
166 text
.delete("insert-1c")
168 # Ick. It may require *inserting* spaces if we back up over a
169 # tab character! This is written to be clear, not fast.
170 tabwidth
= self
.tabwidth
171 have
= len(chars
.expandtabs(tabwidth
))
173 want
= ((have
- 1) // self
.indentwidth
) * self
.indentwidth
177 ncharsdeleted
= ncharsdeleted
+ 1
178 have
= len(chars
.expandtabs(tabwidth
))
179 if have
<= want
or chars
[-1] not in " \t":
181 text
.undo_block_start()
182 text
.delete("insert-%dc" % ncharsdeleted
, "insert")
184 text
.insert("insert", ' ' * (want
- have
))
185 text
.undo_block_stop()
188 def smart_indent_event(self
, event
):
189 # if intraline selection:
191 # elif multiline selection:
192 # do indent-region & return
195 first
, last
= self
.editwin
.get_selection_indices()
196 text
.undo_block_start()
199 if index2line(first
) != index2line(last
):
200 return self
.indent_region_event(event
)
201 text
.delete(first
, last
)
202 text
.mark_set("insert", first
)
203 prefix
= text
.get("insert linestart", "insert")
204 raw
, effective
= classifyws(prefix
, self
.tabwidth
)
205 if raw
== len(prefix
):
206 # only whitespace to the left
207 self
.reindent_to(effective
+ self
.indentwidth
)
212 effective
= len(prefix
.expandtabs(self
.tabwidth
))
214 pad
= ' ' * (n
- effective
% n
)
215 text
.insert("insert", pad
)
219 text
.undo_block_stop()
221 def newline_and_indent_event(self
, event
):
223 first
, last
= self
.editwin
.get_selection_indices()
224 text
.undo_block_start()
227 text
.delete(first
, last
)
228 text
.mark_set("insert", first
)
229 line
= text
.get("insert linestart", "insert")
231 while i
< n
and line
[i
] in " \t":
234 # the cursor is in or at leading indentation; just inject
235 # an empty line at the start
236 text
.insert("insert linestart", '\n')
239 # strip whitespace before insert point
241 while line
and line
[-1] in " \t":
245 text
.delete("insert - %d chars" % i
, "insert")
246 # strip whitespace after insert point
247 while text
.get("insert") in " \t":
248 text
.delete("insert")
250 text
.insert("insert", '\n')
252 # adjust indentation for continuations and block
253 # open/close first need to find the last stmt
254 lno
= index2line(text
.index('insert'))
255 y
= PyParse
.Parser(self
.indentwidth
, self
.tabwidth
)
256 for context
in self
.num_context_lines
:
257 startat
= max(lno
- context
, 1)
258 startatindex
= `startat`
+ ".0"
259 rawtext
= text
.get(startatindex
, "insert")
261 bod
= y
.find_good_parse_start(
262 self
.context_use_ps1
,
263 self
._build
_char
_in
_string
_func
(startatindex
))
264 if bod
is not None or startat
== 1:
267 c
= y
.get_continuation_type()
268 if c
!= PyParse
.C_NONE
:
269 # The current stmt hasn't ended yet.
270 if c
== PyParse
.C_STRING
:
271 # inside a string; just mimic the current indent
272 text
.insert("insert", indent
)
273 elif c
== PyParse
.C_BRACKET
:
274 # line up with the first (if any) element of the
275 # last open bracket structure; else indent one
276 # level beyond the indent of the line with the
278 self
.reindent_to(y
.compute_bracket_indent())
279 elif c
== PyParse
.C_BACKSLASH
:
280 # if more than one line in this stmt already, just
281 # mimic the current indent; else if initial line
282 # has a start on an assignment stmt, indent to
283 # beyond leftmost =; else to beyond first chunk of
284 # non-whitespace on initial line
285 if y
.get_num_lines_in_stmt() > 1:
286 text
.insert("insert", indent
)
288 self
.reindent_to(y
.compute_backslash_indent())
290 assert 0, "bogus continuation type " + `c`
293 # This line starts a brand new stmt; indent relative to
294 # indentation of initial line of closest preceding
296 indent
= y
.get_base_indent_string()
297 text
.insert("insert", indent
)
298 if y
.is_block_opener():
299 self
.smart_indent_event(event
)
300 elif indent
and y
.is_block_closer():
301 self
.smart_backspace_event(event
)
305 text
.undo_block_stop()
307 auto_indent
= newline_and_indent_event
309 # Our editwin provides a is_char_in_string function that works
310 # with a Tk text index, but PyParse only knows about offsets into
311 # a string. This builds a function for PyParse that accepts an
314 def _build_char_in_string_func(self
, startindex
):
315 def inner(offset
, _startindex
=startindex
,
316 _icis
=self
.editwin
.is_char_in_string
):
317 return _icis(_startindex
+ "+%dc" % offset
)
320 def indent_region_event(self
, event
):
321 head
, tail
, chars
, lines
= self
.get_region()
322 for pos
in range(len(lines
)):
325 raw
, effective
= classifyws(line
, self
.tabwidth
)
326 effective
= effective
+ self
.indentwidth
327 lines
[pos
] = self
._make
_blanks
(effective
) + line
[raw
:]
328 self
.set_region(head
, tail
, chars
, lines
)
331 def dedent_region_event(self
, event
):
332 head
, tail
, chars
, lines
= self
.get_region()
333 for pos
in range(len(lines
)):
336 raw
, effective
= classifyws(line
, self
.tabwidth
)
337 effective
= max(effective
- self
.indentwidth
, 0)
338 lines
[pos
] = self
._make
_blanks
(effective
) + line
[raw
:]
339 self
.set_region(head
, tail
, chars
, lines
)
342 def comment_region_event(self
, event
):
343 head
, tail
, chars
, lines
= self
.get_region()
344 for pos
in range(len(lines
) - 1):
346 lines
[pos
] = '##' + line
347 self
.set_region(head
, tail
, chars
, lines
)
349 def uncomment_region_event(self
, event
):
350 head
, tail
, chars
, lines
= self
.get_region()
351 for pos
in range(len(lines
)):
357 elif line
[:1] == '#':
360 self
.set_region(head
, tail
, chars
, lines
)
362 def tabify_region_event(self
, event
):
363 head
, tail
, chars
, lines
= self
.get_region()
364 tabwidth
= self
._asktabwidth
()
365 for pos
in range(len(lines
)):
368 raw
, effective
= classifyws(line
, tabwidth
)
369 ntabs
, nspaces
= divmod(effective
, tabwidth
)
370 lines
[pos
] = '\t' * ntabs
+ ' ' * nspaces
+ line
[raw
:]
371 self
.set_region(head
, tail
, chars
, lines
)
373 def untabify_region_event(self
, event
):
374 head
, tail
, chars
, lines
= self
.get_region()
375 tabwidth
= self
._asktabwidth
()
376 for pos
in range(len(lines
)):
377 lines
[pos
] = lines
[pos
].expandtabs(tabwidth
)
378 self
.set_region(head
, tail
, chars
, lines
)
380 def toggle_tabs_event(self
, event
):
381 if self
.editwin
.askyesno(
383 "Turn tabs " + ("on", "off")[self
.usetabs
] + "?",
385 self
.usetabs
= not self
.usetabs
388 # XXX this isn't bound to anything -- see class tabwidth comments
389 def change_tabwidth_event(self
, event
):
390 new
= self
._asktabwidth
()
391 if new
!= self
.tabwidth
:
393 self
.set_indentation_params(0, guess
=0)
396 def change_indentwidth_event(self
, event
):
397 new
= self
.editwin
.askinteger(
399 "New indent width (1-16)",
401 initialvalue
=self
.indentwidth
,
404 if new
and new
!= self
.indentwidth
:
405 self
.indentwidth
= new
408 def get_region(self
):
410 first
, last
= self
.editwin
.get_selection_indices()
412 head
= text
.index(first
+ " linestart")
413 tail
= text
.index(last
+ "-1c lineend +1c")
415 head
= text
.index("insert linestart")
416 tail
= text
.index("insert lineend +1c")
417 chars
= text
.get(head
, tail
)
418 lines
= chars
.split("\n")
419 return head
, tail
, chars
, lines
421 def set_region(self
, head
, tail
, chars
, lines
):
423 newchars
= "\n".join(lines
)
424 if newchars
== chars
:
427 text
.tag_remove("sel", "1.0", "end")
428 text
.mark_set("insert", head
)
429 text
.undo_block_start()
430 text
.delete(head
, tail
)
431 text
.insert(head
, newchars
)
432 text
.undo_block_stop()
433 text
.tag_add("sel", head
, "insert")
435 # Make string that displays as n leading blanks.
437 def _make_blanks(self
, n
):
439 ntabs
, nspaces
= divmod(n
, self
.tabwidth
)
440 return '\t' * ntabs
+ ' ' * nspaces
444 # Delete from beginning of line to insert point, then reinsert
445 # column logical (meaning use tabs if appropriate) spaces.
447 def reindent_to(self
, column
):
449 text
.undo_block_start()
450 if text
.compare("insert linestart", "!=", "insert"):
451 text
.delete("insert linestart", "insert")
453 text
.insert("insert", self
._make
_blanks
(column
))
454 text
.undo_block_stop()
456 def _asktabwidth(self
):
457 return self
.editwin
.askinteger(
461 initialvalue
=self
.tabwidth
,
463 maxvalue
=16) or self
.tabwidth
465 # Guess indentwidth from text content.
466 # Return guessed indentwidth. This should not be believed unless
467 # it's in a reasonable range (e.g., it will be 0 if no indented
470 def guess_indent(self
):
471 opener
, indented
= IndentSearcher(self
.text
, self
.tabwidth
).run()
472 if opener
and indented
:
473 raw
, indentsmall
= classifyws(opener
, self
.tabwidth
)
474 raw
, indentlarge
= classifyws(indented
, self
.tabwidth
)
476 indentsmall
= indentlarge
= 0
477 return indentlarge
- indentsmall
479 # "line.col" -> line, as an int
480 def index2line(index
):
481 return int(float(index
))
483 # Look at the leading whitespace in s.
484 # Return pair (# of leading ws characters,
485 # effective # of leading blanks after expanding
486 # tabs to width tabwidth)
488 def classifyws(s
, tabwidth
):
493 effective
= effective
+ 1
496 effective
= (effective
// tabwidth
+ 1) * tabwidth
499 return raw
, effective
505 class IndentSearcher
:
507 # .run() chews over the Text widget, looking for a block opener
508 # and the stmt following it. Returns a pair,
509 # (line containing block opener, line containing stmt)
510 # Either or both may be None.
512 def __init__(self
, text
, tabwidth
):
514 self
.tabwidth
= tabwidth
515 self
.i
= self
.finished
= 0
516 self
.blkopenline
= self
.indentedline
= None
521 i
= self
.i
= self
.i
+ 1
523 if self
.text
.compare(mark
, ">=", "end"):
525 return self
.text
.get(mark
, mark
+ " lineend+1c")
527 def tokeneater(self
, type, token
, start
, end
, line
,
528 INDENT
=_tokenize
.INDENT
,
530 OPENERS
=('class', 'def', 'for', 'if', 'try', 'while')):
533 elif type == NAME
and token
in OPENERS
:
534 self
.blkopenline
= line
535 elif type == INDENT
and self
.blkopenline
:
536 self
.indentedline
= line
540 save_tabsize
= _tokenize
.tabsize
541 _tokenize
.tabsize
= self
.tabwidth
544 _tokenize
.tokenize(self
.readline
, self
.tokeneater
)
545 except _tokenize
.TokenError
:
546 # since we cut off the tokenizer early, we can trigger
550 _tokenize
.tabsize
= save_tabsize
551 return self
.blkopenline
, self
.indentedline