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>>
43 ('format', [ # /s/edit/format dscherer@cmu.edu
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>>': ['<Shift-Tab>', # dscherer@cmu.edu
65 '<Control-bracketleft>'],
66 '<<comment-region>>': ['<Alt-Key-3>'],
67 '<<uncomment-region>>': ['<Alt-Key-4>'],
68 '<<tabify-region>>': ['<Alt-Key-5>'],
69 '<<untabify-region>>': ['<Alt-Key-6>'],
70 '<<toggle-tabs>>': ['<Alt-Key-t>'],
71 '<<change-indentwidth>>': ['<Alt-Key-u>'],
75 '<<indent-region>>': ['<Alt-bracketright>',
76 '<Meta-bracketright>',
77 '<Control-bracketright>'],
78 '<<dedent-region>>': ['<Alt-bracketleft>',
80 '<Control-bracketleft>'],
81 '<<comment-region>>': ['<Alt-Key-3>', '<Meta-Key-3>'],
82 '<<uncomment-region>>': ['<Alt-Key-4>', '<Meta-Key-4>'],
83 '<<tabify-region>>': ['<Alt-Key-5>', '<Meta-Key-5>'],
84 '<<untabify-region>>': ['<Alt-Key-6>', '<Meta-Key-6>'],
85 '<<toggle-tabs>>': ['<Alt-Key-t>'],
86 '<<change-indentwidth>>': ['<Alt-Key-u>'],
89 # usetabs true -> literal tab characters are used by indent and
90 # dedent cmds, possibly mixed with spaces if
91 # indentwidth is not a multiple of tabwidth
92 # false -> tab characters are converted to spaces by indent
93 # and dedent cmds, and ditto TAB keystrokes
94 # indentwidth is the number of characters per logical indent level.
95 # tabwidth is the display width of a literal tab character.
96 # CAUTION: telling Tk to use anything other than its default
97 # tab setting causes it to use an entirely different tabbing algorithm,
98 # treating tab stops as fixed distances from the left margin.
99 # Nobody expects this, so for now tabwidth should never be changed.
102 tabwidth
= 8 # for IDLE use, must remain 8 until Tk is fixed
104 # If context_use_ps1 is true, parsing searches back for a ps1 line;
105 # else searches for a popular (if, def, ...) Python stmt.
108 # When searching backwards for a reliable place to begin parsing,
109 # first start num_context_lines[0] lines back, then
110 # num_context_lines[1] lines back if that didn't work, and so on.
111 # The last value should be huge (larger than the # of lines in a
113 # Making the initial values larger slows things down more often.
114 num_context_lines
= 50, 500, 5000000
116 def __init__(self
, editwin
):
117 self
.editwin
= editwin
118 self
.text
= editwin
.text
120 def config(self
, **options
):
121 for key
, value
in options
.items():
124 elif key
== 'indentwidth':
125 self
.indentwidth
= value
126 elif key
== 'tabwidth':
127 self
.tabwidth
= value
128 elif key
== 'context_use_ps1':
129 self
.context_use_ps1
= value
131 raise KeyError, "bad option name: %s" % `key`
133 # If ispythonsource and guess are true, guess a good value for
134 # indentwidth based on file content (if possible), and if
135 # indentwidth != tabwidth set usetabs false.
136 # In any case, adjust the Text widget's view of what a tab
139 def set_indentation_params(self
, ispythonsource
, guess
=1):
140 if guess
and ispythonsource
:
141 i
= self
.guess_indent()
144 if self
.indentwidth
!= self
.tabwidth
:
147 self
.editwin
.set_tabwidth(self
.tabwidth
)
149 def smart_backspace_event(self
, event
):
151 first
, last
= self
.editwin
.get_selection_indices()
153 text
.delete(first
, last
)
154 text
.mark_set("insert", first
)
156 # Delete whitespace left, until hitting a real char or closest
157 # preceding virtual tab stop.
158 chars
= text
.get("insert linestart", "insert")
160 if text
.compare("insert", ">", "1.0"):
161 # easy: delete preceding newline
162 text
.delete("insert-1c")
164 text
.bell() # at start of buffer
166 if chars
[-1] not in " \t":
167 # easy: delete preceding real char
168 text
.delete("insert-1c")
170 # Ick. It may require *inserting* spaces if we back up over a
171 # tab character! This is written to be clear, not fast.
172 expand
, tabwidth
= string
.expandtabs
, self
.tabwidth
173 have
= len(expand(chars
, tabwidth
))
175 want
= int((have
- 1) / self
.indentwidth
) * self
.indentwidth
179 ncharsdeleted
= ncharsdeleted
+ 1
180 have
= len(expand(chars
, tabwidth
))
181 if have
<= want
or chars
[-1] not in " \t":
183 text
.undo_block_start()
184 text
.delete("insert-%dc" % ncharsdeleted
, "insert")
186 text
.insert("insert", ' ' * (want
- have
))
187 text
.undo_block_stop()
190 def smart_indent_event(self
, event
):
191 # if intraline selection:
193 # elif multiline selection:
194 # do indent-region & return
197 first
, last
= self
.editwin
.get_selection_indices()
198 text
.undo_block_start()
201 if index2line(first
) != index2line(last
):
202 return self
.indent_region_event(event
)
203 text
.delete(first
, last
)
204 text
.mark_set("insert", first
)
205 prefix
= text
.get("insert linestart", "insert")
206 raw
, effective
= classifyws(prefix
, self
.tabwidth
)
207 if raw
== len(prefix
):
208 # only whitespace to the left
209 self
.reindent_to(effective
+ self
.indentwidth
)
214 effective
= len(string
.expandtabs(prefix
,
217 pad
= ' ' * (n
- effective
% n
)
218 text
.insert("insert", pad
)
222 text
.undo_block_stop()
224 def newline_and_indent_event(self
, event
):
226 first
, last
= self
.editwin
.get_selection_indices()
227 text
.undo_block_start()
230 text
.delete(first
, last
)
231 text
.mark_set("insert", first
)
232 line
= text
.get("insert linestart", "insert")
234 while i
< n
and line
[i
] in " \t":
237 # the cursor is in or at leading indentation; just inject
238 # an empty line at the start
239 text
.insert("insert linestart", '\n')
242 # strip whitespace before insert point
244 while line
and line
[-1] in " \t":
248 text
.delete("insert - %d chars" % i
, "insert")
249 # strip whitespace after insert point
250 while text
.get("insert") in " \t":
251 text
.delete("insert")
253 text
.insert("insert", '\n')
255 # adjust indentation for continuations and block
256 # open/close first need to find the last stmt
257 lno
= index2line(text
.index('insert'))
258 y
= PyParse
.Parser(self
.indentwidth
, self
.tabwidth
)
259 for context
in self
.num_context_lines
:
260 startat
= max(lno
- context
, 1)
261 startatindex
= `startat`
+ ".0"
262 rawtext
= text
.get(startatindex
, "insert")
264 bod
= y
.find_good_parse_start(
265 self
.context_use_ps1
,
266 self
._build
_char
_in
_string
_func
(startatindex
))
267 if bod
is not None or startat
== 1:
270 c
= y
.get_continuation_type()
271 if c
!= PyParse
.C_NONE
:
272 # The current stmt hasn't ended yet.
273 if c
== PyParse
.C_STRING
:
274 # inside a string; just mimic the current indent
275 text
.insert("insert", indent
)
276 elif c
== PyParse
.C_BRACKET
:
277 # line up with the first (if any) element of the
278 # last open bracket structure; else indent one
279 # level beyond the indent of the line with the
281 self
.reindent_to(y
.compute_bracket_indent())
282 elif c
== PyParse
.C_BACKSLASH
:
283 # if more than one line in this stmt already, just
284 # mimic the current indent; else if initial line
285 # has a start on an assignment stmt, indent to
286 # beyond leftmost =; else to beyond first chunk of
287 # non-whitespace on initial line
288 if y
.get_num_lines_in_stmt() > 1:
289 text
.insert("insert", indent
)
291 self
.reindent_to(y
.compute_backslash_indent())
293 assert 0, "bogus continuation type " + `c`
296 # This line starts a brand new stmt; indent relative to
297 # indentation of initial line of closest preceding
299 indent
= y
.get_base_indent_string()
300 text
.insert("insert", indent
)
301 if y
.is_block_opener():
302 self
.smart_indent_event(event
)
303 elif indent
and y
.is_block_closer():
304 self
.smart_backspace_event(event
)
308 text
.undo_block_stop()
310 auto_indent
= newline_and_indent_event
312 # Our editwin provides a is_char_in_string function that works
313 # with a Tk text index, but PyParse only knows about offsets into
314 # a string. This builds a function for PyParse that accepts an
317 def _build_char_in_string_func(self
, startindex
):
318 def inner(offset
, _startindex
=startindex
,
319 _icis
=self
.editwin
.is_char_in_string
):
320 return _icis(_startindex
+ "+%dc" % offset
)
323 def indent_region_event(self
, event
):
324 head
, tail
, chars
, lines
= self
.get_region()
325 for pos
in range(len(lines
)):
328 raw
, effective
= classifyws(line
, self
.tabwidth
)
329 effective
= effective
+ self
.indentwidth
330 lines
[pos
] = self
._make
_blanks
(effective
) + line
[raw
:]
331 self
.set_region(head
, tail
, chars
, lines
)
334 def dedent_region_event(self
, event
):
335 head
, tail
, chars
, lines
= self
.get_region()
336 for pos
in range(len(lines
)):
339 raw
, effective
= classifyws(line
, self
.tabwidth
)
340 effective
= max(effective
- self
.indentwidth
, 0)
341 lines
[pos
] = self
._make
_blanks
(effective
) + line
[raw
:]
342 self
.set_region(head
, tail
, chars
, lines
)
345 def comment_region_event(self
, event
):
346 head
, tail
, chars
, lines
= self
.get_region()
347 for pos
in range(len(lines
) - 1):
349 lines
[pos
] = '##' + line
350 self
.set_region(head
, tail
, chars
, lines
)
352 def uncomment_region_event(self
, event
):
353 head
, tail
, chars
, lines
= self
.get_region()
354 for pos
in range(len(lines
)):
360 elif line
[:1] == '#':
363 self
.set_region(head
, tail
, chars
, lines
)
365 def tabify_region_event(self
, event
):
366 head
, tail
, chars
, lines
= self
.get_region()
367 tabwidth
= self
._asktabwidth
()
368 for pos
in range(len(lines
)):
371 raw
, effective
= classifyws(line
, tabwidth
)
372 ntabs
, nspaces
= divmod(effective
, tabwidth
)
373 lines
[pos
] = '\t' * ntabs
+ ' ' * nspaces
+ line
[raw
:]
374 self
.set_region(head
, tail
, chars
, lines
)
376 def untabify_region_event(self
, event
):
377 head
, tail
, chars
, lines
= self
.get_region()
378 tabwidth
= self
._asktabwidth
()
379 for pos
in range(len(lines
)):
380 lines
[pos
] = string
.expandtabs(lines
[pos
], tabwidth
)
381 self
.set_region(head
, tail
, chars
, lines
)
383 def toggle_tabs_event(self
, event
):
384 if self
.editwin
.askyesno(
386 "Turn tabs " + ("on", "off")[self
.usetabs
] + "?",
388 self
.usetabs
= not self
.usetabs
391 # XXX this isn't bound to anything -- see class tabwidth comments
392 def change_tabwidth_event(self
, event
):
393 new
= self
._asktabwidth
()
394 if new
!= self
.tabwidth
:
396 self
.set_indentation_params(0, guess
=0)
399 def change_indentwidth_event(self
, event
):
400 new
= self
.editwin
.askinteger(
402 "New indent width (1-16)",
404 initialvalue
=self
.indentwidth
,
407 if new
and new
!= self
.indentwidth
:
408 self
.indentwidth
= new
411 def get_region(self
):
413 first
, last
= self
.editwin
.get_selection_indices()
415 head
= text
.index(first
+ " linestart")
416 tail
= text
.index(last
+ "-1c lineend +1c")
418 head
= text
.index("insert linestart")
419 tail
= text
.index("insert lineend +1c")
420 chars
= text
.get(head
, tail
)
421 lines
= string
.split(chars
, "\n")
422 return head
, tail
, chars
, lines
424 def set_region(self
, head
, tail
, chars
, lines
):
426 newchars
= string
.join(lines
, "\n")
427 if newchars
== chars
:
430 text
.tag_remove("sel", "1.0", "end")
431 text
.mark_set("insert", head
)
432 text
.undo_block_start()
433 text
.delete(head
, tail
)
434 text
.insert(head
, newchars
)
435 text
.undo_block_stop()
436 text
.tag_add("sel", head
, "insert")
438 # Make string that displays as n leading blanks.
440 def _make_blanks(self
, n
):
442 ntabs
, nspaces
= divmod(n
, self
.tabwidth
)
443 return '\t' * ntabs
+ ' ' * nspaces
447 # Delete from beginning of line to insert point, then reinsert
448 # column logical (meaning use tabs if appropriate) spaces.
450 def reindent_to(self
, column
):
452 text
.undo_block_start()
453 if text
.compare("insert linestart", "!=", "insert"):
454 text
.delete("insert linestart", "insert")
456 text
.insert("insert", self
._make
_blanks
(column
))
457 text
.undo_block_stop()
459 def _asktabwidth(self
):
460 return self
.editwin
.askinteger(
464 initialvalue
=self
.tabwidth
,
466 maxvalue
=16) or self
.tabwidth
468 # Guess indentwidth from text content.
469 # Return guessed indentwidth. This should not be believed unless
470 # it's in a reasonable range (e.g., it will be 0 if no indented
473 def guess_indent(self
):
474 opener
, indented
= IndentSearcher(self
.text
, self
.tabwidth
).run()
475 if opener
and indented
:
476 raw
, indentsmall
= classifyws(opener
, self
.tabwidth
)
477 raw
, indentlarge
= classifyws(indented
, self
.tabwidth
)
479 indentsmall
= indentlarge
= 0
480 return indentlarge
- indentsmall
482 # "line.col" -> line, as an int
483 def index2line(index
):
484 return int(float(index
))
486 # Look at the leading whitespace in s.
487 # Return pair (# of leading ws characters,
488 # effective # of leading blanks after expanding
489 # tabs to width tabwidth)
491 def classifyws(s
, tabwidth
):
496 effective
= effective
+ 1
499 effective
= (effective
/ tabwidth
+ 1) * tabwidth
502 return raw
, effective
508 class IndentSearcher
:
510 # .run() chews over the Text widget, looking for a block opener
511 # and the stmt following it. Returns a pair,
512 # (line containing block opener, line containing stmt)
513 # Either or both may be None.
515 def __init__(self
, text
, tabwidth
):
517 self
.tabwidth
= tabwidth
518 self
.i
= self
.finished
= 0
519 self
.blkopenline
= self
.indentedline
= None
524 i
= self
.i
= self
.i
+ 1
526 if self
.text
.compare(mark
, ">=", "end"):
528 return self
.text
.get(mark
, mark
+ " lineend+1c")
530 def tokeneater(self
, type, token
, start
, end
, line
,
531 INDENT
=_tokenize
.INDENT
,
533 OPENERS
=('class', 'def', 'for', 'if', 'try', 'while')):
536 elif type == NAME
and token
in OPENERS
:
537 self
.blkopenline
= line
538 elif type == INDENT
and self
.blkopenline
:
539 self
.indentedline
= line
543 save_tabsize
= _tokenize
.tabsize
544 _tokenize
.tabsize
= self
.tabwidth
547 _tokenize
.tokenize(self
.readline
, self
.tokeneater
)
548 except _tokenize
.TokenError
:
549 # since we cut off the tokenizer early, we can trigger
553 _tokenize
.tabsize
= save_tabsize
554 return self
.blkopenline
, self
.indentedline