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
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>>'),
55 # usetabs true -> literal tab characters are used by indent and
56 # dedent cmds, possibly mixed with spaces if
57 # indentwidth is not a multiple of tabwidth
58 # false -> tab characters are converted to spaces by indent
59 # and dedent cmds, and ditto TAB keystrokes
60 # indentwidth is the number of characters per logical indent level.
61 # tabwidth is the display width of a literal tab character.
62 # CAUTION: telling Tk to use anything other than its default
63 # tab setting causes it to use an entirely different tabbing algorithm,
64 # treating tab stops as fixed distances from the left margin.
65 # Nobody expects this, so for now tabwidth should never be changed.
68 tabwidth
= 8 # for IDLE use, must remain 8 until Tk is fixed
70 # If context_use_ps1 is true, parsing searches back for a ps1 line;
71 # else searches for a popular (if, def, ...) Python stmt.
74 # When searching backwards for a reliable place to begin parsing,
75 # first start num_context_lines[0] lines back, then
76 # num_context_lines[1] lines back if that didn't work, and so on.
77 # The last value should be huge (larger than the # of lines in a
79 # Making the initial values larger slows things down more often.
80 num_context_lines
= 50, 500, 5000000
82 def __init__(self
, editwin
):
83 self
.editwin
= editwin
84 self
.text
= editwin
.text
86 def config(self
, **options
):
87 for key
, value
in options
.items():
90 elif key
== 'indentwidth':
91 self
.indentwidth
= value
92 elif key
== 'tabwidth':
94 elif key
== 'context_use_ps1':
95 self
.context_use_ps1
= value
97 raise KeyError, "bad option name: %s" % `key`
99 # If ispythonsource and guess are true, guess a good value for
100 # indentwidth based on file content (if possible), and if
101 # indentwidth != tabwidth set usetabs false.
102 # In any case, adjust the Text widget's view of what a tab
105 def set_indentation_params(self
, ispythonsource
, guess
=1):
106 if guess
and ispythonsource
:
107 i
= self
.guess_indent()
110 if self
.indentwidth
!= self
.tabwidth
:
113 self
.editwin
.set_tabwidth(self
.tabwidth
)
115 def smart_backspace_event(self
, event
):
117 first
, last
= self
.editwin
.get_selection_indices()
119 text
.delete(first
, last
)
120 text
.mark_set("insert", first
)
122 # Delete whitespace left, until hitting a real char or closest
123 # preceding virtual tab stop.
124 chars
= text
.get("insert linestart", "insert")
126 if text
.compare("insert", ">", "1.0"):
127 # easy: delete preceding newline
128 text
.delete("insert-1c")
130 text
.bell() # at start of buffer
132 if chars
[-1] not in " \t":
133 # easy: delete preceding real char
134 text
.delete("insert-1c")
136 # Ick. It may require *inserting* spaces if we back up over a
137 # tab character! This is written to be clear, not fast.
138 expand
, tabwidth
= string
.expandtabs
, self
.tabwidth
139 have
= len(expand(chars
, tabwidth
))
141 want
= ((have
- 1) // self
.indentwidth
) * self
.indentwidth
145 ncharsdeleted
= ncharsdeleted
+ 1
146 have
= len(expand(chars
, tabwidth
))
147 if have
<= want
or chars
[-1] not in " \t":
149 text
.undo_block_start()
150 text
.delete("insert-%dc" % ncharsdeleted
, "insert")
152 text
.insert("insert", ' ' * (want
- have
))
153 text
.undo_block_stop()
156 def smart_indent_event(self
, event
):
157 # if intraline selection:
159 # elif multiline selection:
160 # do indent-region & return
163 first
, last
= self
.editwin
.get_selection_indices()
164 text
.undo_block_start()
167 if index2line(first
) != index2line(last
):
168 return self
.indent_region_event(event
)
169 text
.delete(first
, last
)
170 text
.mark_set("insert", first
)
171 prefix
= text
.get("insert linestart", "insert")
172 raw
, effective
= classifyws(prefix
, self
.tabwidth
)
173 if raw
== len(prefix
):
174 # only whitespace to the left
175 self
.reindent_to(effective
+ self
.indentwidth
)
180 effective
= len(string
.expandtabs(prefix
,
183 pad
= ' ' * (n
- effective
% n
)
184 text
.insert("insert", pad
)
188 text
.undo_block_stop()
190 def newline_and_indent_event(self
, event
):
192 first
, last
= self
.editwin
.get_selection_indices()
193 text
.undo_block_start()
196 text
.delete(first
, last
)
197 text
.mark_set("insert", first
)
198 line
= text
.get("insert linestart", "insert")
200 while i
< n
and line
[i
] in " \t":
203 # the cursor is in or at leading indentation; just inject
204 # an empty line at the start
205 text
.insert("insert linestart", '\n')
208 # strip whitespace before insert point
210 while line
and line
[-1] in " \t":
214 text
.delete("insert - %d chars" % i
, "insert")
215 # strip whitespace after insert point
216 while text
.get("insert") in " \t":
217 text
.delete("insert")
219 text
.insert("insert", '\n')
221 # adjust indentation for continuations and block
222 # open/close first need to find the last stmt
223 lno
= index2line(text
.index('insert'))
224 y
= PyParse
.Parser(self
.indentwidth
, self
.tabwidth
)
225 for context
in self
.num_context_lines
:
226 startat
= max(lno
- context
, 1)
227 startatindex
= `startat`
+ ".0"
228 rawtext
= text
.get(startatindex
, "insert")
230 bod
= y
.find_good_parse_start(
231 self
.context_use_ps1
,
232 self
._build
_char
_in
_string
_func
(startatindex
))
233 if bod
is not None or startat
== 1:
236 c
= y
.get_continuation_type()
237 if c
!= PyParse
.C_NONE
:
238 # The current stmt hasn't ended yet.
239 if c
== PyParse
.C_STRING
:
240 # inside a string; just mimic the current indent
241 text
.insert("insert", indent
)
242 elif c
== PyParse
.C_BRACKET
:
243 # line up with the first (if any) element of the
244 # last open bracket structure; else indent one
245 # level beyond the indent of the line with the
247 self
.reindent_to(y
.compute_bracket_indent())
248 elif c
== PyParse
.C_BACKSLASH
:
249 # if more than one line in this stmt already, just
250 # mimic the current indent; else if initial line
251 # has a start on an assignment stmt, indent to
252 # beyond leftmost =; else to beyond first chunk of
253 # non-whitespace on initial line
254 if y
.get_num_lines_in_stmt() > 1:
255 text
.insert("insert", indent
)
257 self
.reindent_to(y
.compute_backslash_indent())
259 assert 0, "bogus continuation type " + `c`
262 # This line starts a brand new stmt; indent relative to
263 # indentation of initial line of closest preceding
265 indent
= y
.get_base_indent_string()
266 text
.insert("insert", indent
)
267 if y
.is_block_opener():
268 self
.smart_indent_event(event
)
269 elif indent
and y
.is_block_closer():
270 self
.smart_backspace_event(event
)
274 text
.undo_block_stop()
276 auto_indent
= newline_and_indent_event
278 # Our editwin provides a is_char_in_string function that works
279 # with a Tk text index, but PyParse only knows about offsets into
280 # a string. This builds a function for PyParse that accepts an
283 def _build_char_in_string_func(self
, startindex
):
284 def inner(offset
, _startindex
=startindex
,
285 _icis
=self
.editwin
.is_char_in_string
):
286 return _icis(_startindex
+ "+%dc" % offset
)
289 def indent_region_event(self
, event
):
290 head
, tail
, chars
, lines
= self
.get_region()
291 for pos
in range(len(lines
)):
294 raw
, effective
= classifyws(line
, self
.tabwidth
)
295 effective
= effective
+ self
.indentwidth
296 lines
[pos
] = self
._make
_blanks
(effective
) + line
[raw
:]
297 self
.set_region(head
, tail
, chars
, lines
)
300 def dedent_region_event(self
, event
):
301 head
, tail
, chars
, lines
= self
.get_region()
302 for pos
in range(len(lines
)):
305 raw
, effective
= classifyws(line
, self
.tabwidth
)
306 effective
= max(effective
- self
.indentwidth
, 0)
307 lines
[pos
] = self
._make
_blanks
(effective
) + line
[raw
:]
308 self
.set_region(head
, tail
, chars
, lines
)
311 def comment_region_event(self
, event
):
312 head
, tail
, chars
, lines
= self
.get_region()
313 for pos
in range(len(lines
) - 1):
315 lines
[pos
] = '##' + line
316 self
.set_region(head
, tail
, chars
, lines
)
318 def uncomment_region_event(self
, event
):
319 head
, tail
, chars
, lines
= self
.get_region()
320 for pos
in range(len(lines
)):
326 elif line
[:1] == '#':
329 self
.set_region(head
, tail
, chars
, lines
)
331 def tabify_region_event(self
, event
):
332 head
, tail
, chars
, lines
= self
.get_region()
333 tabwidth
= self
._asktabwidth
()
334 for pos
in range(len(lines
)):
337 raw
, effective
= classifyws(line
, tabwidth
)
338 ntabs
, nspaces
= divmod(effective
, tabwidth
)
339 lines
[pos
] = '\t' * ntabs
+ ' ' * nspaces
+ line
[raw
:]
340 self
.set_region(head
, tail
, chars
, lines
)
342 def untabify_region_event(self
, event
):
343 head
, tail
, chars
, lines
= self
.get_region()
344 tabwidth
= self
._asktabwidth
()
345 for pos
in range(len(lines
)):
346 lines
[pos
] = string
.expandtabs(lines
[pos
], tabwidth
)
347 self
.set_region(head
, tail
, chars
, lines
)
349 def toggle_tabs_event(self
, event
):
350 if self
.editwin
.askyesno(
352 "Turn tabs " + ("on", "off")[self
.usetabs
] + "?",
354 self
.usetabs
= not self
.usetabs
357 # XXX this isn't bound to anything -- see class tabwidth comments
358 def change_tabwidth_event(self
, event
):
359 new
= self
._asktabwidth
()
360 if new
!= self
.tabwidth
:
362 self
.set_indentation_params(0, guess
=0)
365 def change_indentwidth_event(self
, event
):
366 new
= self
.editwin
.askinteger(
368 "New indent width (1-16)",
370 initialvalue
=self
.indentwidth
,
373 if new
and new
!= self
.indentwidth
:
374 self
.indentwidth
= new
377 def get_region(self
):
379 first
, last
= self
.editwin
.get_selection_indices()
381 head
= text
.index(first
+ " linestart")
382 tail
= text
.index(last
+ "-1c lineend +1c")
384 head
= text
.index("insert linestart")
385 tail
= text
.index("insert lineend +1c")
386 chars
= text
.get(head
, tail
)
387 lines
= string
.split(chars
, "\n")
388 return head
, tail
, chars
, lines
390 def set_region(self
, head
, tail
, chars
, lines
):
392 newchars
= string
.join(lines
, "\n")
393 if newchars
== chars
:
396 text
.tag_remove("sel", "1.0", "end")
397 text
.mark_set("insert", head
)
398 text
.undo_block_start()
399 text
.delete(head
, tail
)
400 text
.insert(head
, newchars
)
401 text
.undo_block_stop()
402 text
.tag_add("sel", head
, "insert")
404 # Make string that displays as n leading blanks.
406 def _make_blanks(self
, n
):
408 ntabs
, nspaces
= divmod(n
, self
.tabwidth
)
409 return '\t' * ntabs
+ ' ' * nspaces
413 # Delete from beginning of line to insert point, then reinsert
414 # column logical (meaning use tabs if appropriate) spaces.
416 def reindent_to(self
, column
):
418 text
.undo_block_start()
419 if text
.compare("insert linestart", "!=", "insert"):
420 text
.delete("insert linestart", "insert")
422 text
.insert("insert", self
._make
_blanks
(column
))
423 text
.undo_block_stop()
425 def _asktabwidth(self
):
426 return self
.editwin
.askinteger(
430 initialvalue
=self
.tabwidth
,
432 maxvalue
=16) or self
.tabwidth
434 # Guess indentwidth from text content.
435 # Return guessed indentwidth. This should not be believed unless
436 # it's in a reasonable range (e.g., it will be 0 if no indented
439 def guess_indent(self
):
440 opener
, indented
= IndentSearcher(self
.text
, self
.tabwidth
).run()
441 if opener
and indented
:
442 raw
, indentsmall
= classifyws(opener
, self
.tabwidth
)
443 raw
, indentlarge
= classifyws(indented
, self
.tabwidth
)
445 indentsmall
= indentlarge
= 0
446 return indentlarge
- indentsmall
448 # "line.col" -> line, as an int
449 def index2line(index
):
450 return int(float(index
))
452 # Look at the leading whitespace in s.
453 # Return pair (# of leading ws characters,
454 # effective # of leading blanks after expanding
455 # tabs to width tabwidth)
457 def classifyws(s
, tabwidth
):
462 effective
= effective
+ 1
465 effective
= (effective
// tabwidth
+ 1) * tabwidth
468 return raw
, effective
474 class IndentSearcher
:
476 # .run() chews over the Text widget, looking for a block opener
477 # and the stmt following it. Returns a pair,
478 # (line containing block opener, line containing stmt)
479 # Either or both may be None.
481 def __init__(self
, text
, tabwidth
):
483 self
.tabwidth
= tabwidth
484 self
.i
= self
.finished
= 0
485 self
.blkopenline
= self
.indentedline
= None
490 i
= self
.i
= self
.i
+ 1
492 if self
.text
.compare(mark
, ">=", "end"):
494 return self
.text
.get(mark
, mark
+ " lineend+1c")
496 def tokeneater(self
, type, token
, start
, end
, line
,
497 INDENT
=_tokenize
.INDENT
,
499 OPENERS
=('class', 'def', 'for', 'if', 'try', 'while')):
502 elif type == NAME
and token
in OPENERS
:
503 self
.blkopenline
= line
504 elif type == INDENT
and self
.blkopenline
:
505 self
.indentedline
= line
509 save_tabsize
= _tokenize
.tabsize
510 _tokenize
.tabsize
= self
.tabwidth
513 _tokenize
.tokenize(self
.readline
, self
.tokeneater
)
514 except _tokenize
.TokenError
:
515 # since we cut off the tokenizer early, we can trigger
519 _tokenize
.tabsize
= save_tabsize
520 return self
.blkopenline
, self
.indentedline