AutoIndent.py 20 KB
Newer Older
1
import string
2 3 4
#from Tkinter import TclError
#import tkMessageBox
#import tkSimpleDialog
5

Guido van Rossum's avatar
Guido van Rossum committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
###$ event <<newline-and-indent>>
###$ win <Key-Return>
###$ win <KP_Enter>
###$ unix <Key-Return>
###$ unix <KP_Enter>

###$ event <<indent-region>>
###$ win <Control-bracketright>
###$ unix <Alt-bracketright>
###$ unix <Control-bracketright>

###$ event <<dedent-region>>
###$ win <Control-bracketleft>
###$ unix <Alt-bracketleft>
###$ unix <Control-bracketleft>

###$ event <<comment-region>>
###$ win <Alt-Key-3>
###$ unix <Alt-Key-3>

###$ event <<uncomment-region>>
###$ win <Alt-Key-4>
###$ unix <Alt-Key-4>

###$ event <<tabify-region>>
###$ win <Alt-Key-5>
###$ unix <Alt-Key-5>

###$ event <<untabify-region>>
###$ win <Alt-Key-6>
###$ unix <Alt-Key-6>
37

Guido van Rossum's avatar
Guido van Rossum committed
38 39
import PyParse

40 41
class AutoIndent:

Guido van Rossum's avatar
Guido van Rossum committed
42 43 44 45 46 47 48 49 50
    menudefs = [
        ('edit', [
            None,
            ('_Indent region', '<<indent-region>>'),
            ('_Dedent region', '<<dedent-region>>'),
            ('Comment _out region', '<<comment-region>>'),
            ('U_ncomment region', '<<uncomment-region>>'),
            ('Tabify region', '<<tabify-region>>'),
            ('Untabify region', '<<untabify-region>>'),
51 52
            ('Toggle tabs', '<<toggle-tabs>>'),
            ('New indent width', '<<change-indentwidth>>'),
Guido van Rossum's avatar
Guido van Rossum committed
53 54 55
        ]),
    ]

56 57
    keydefs = {
        '<<smart-backspace>>': ['<Key-BackSpace>'],
Guido van Rossum's avatar
Guido van Rossum committed
58
        '<<newline-and-indent>>': ['<Key-Return>', '<KP_Enter>'],
59
        '<<smart-indent>>': ['<Key-Tab>']
60 61 62
    }

    windows_keydefs = {
Guido van Rossum's avatar
Guido van Rossum committed
63 64 65 66 67 68
        '<<indent-region>>': ['<Control-bracketright>'],
        '<<dedent-region>>': ['<Control-bracketleft>'],
        '<<comment-region>>': ['<Alt-Key-3>'],
        '<<uncomment-region>>': ['<Alt-Key-4>'],
        '<<tabify-region>>': ['<Alt-Key-5>'],
        '<<untabify-region>>': ['<Alt-Key-6>'],
69
        '<<toggle-tabs>>': ['<Alt-Key-t>'],
70
        '<<change-indentwidth>>': ['<Alt-Key-u>'],
Guido van Rossum's avatar
Guido van Rossum committed
71 72 73 74 75 76 77 78 79 80 81 82 83
    }

    unix_keydefs = {
        '<<indent-region>>': ['<Alt-bracketright>',
                              '<Meta-bracketright>',
                              '<Control-bracketright>'],
        '<<dedent-region>>': ['<Alt-bracketleft>',
                              '<Meta-bracketleft>',
                              '<Control-bracketleft>'],
        '<<comment-region>>': ['<Alt-Key-3>', '<Meta-Key-3>'],
        '<<uncomment-region>>': ['<Alt-Key-4>', '<Meta-Key-4>'],
        '<<tabify-region>>': ['<Alt-Key-5>', '<Meta-Key-5>'],
        '<<untabify-region>>': ['<Alt-Key-6>', '<Meta-Key-6>'],
84 85
        '<<toggle-tabs>>': ['<Alt-Key-t>'],
        '<<change-indentwidth>>': ['<Alt-Key-u>'],
Guido van Rossum's avatar
Guido van Rossum committed
86 87
    }

88 89 90 91 92
    # usetabs true  -> literal tab characters are used by indent and
    #                  dedent cmds, possibly mixed with spaces if
    #                  indentwidth is not a multiple of tabwidth
    #         false -> tab characters are converted to spaces by indent
    #                  and dedent cmds, and ditto TAB keystrokes
93 94 95 96 97 98
    # indentwidth is the number of characters per logical indent level.
    # tabwidth is the display width of a literal tab character.
    # CAUTION:  telling Tk to use anything other than its default
    # tab setting causes it to use an entirely different tabbing algorithm,
    # treating tab stops as fixed distances from the left margin.
    # Nobody expects this, so for now tabwidth should never be changed.
Guido van Rossum's avatar
Guido van Rossum committed
99
    usetabs = 1
100
    indentwidth = 4
101
    tabwidth = 8    # for IDLE use, must remain 8 until Tk is fixed
Guido van Rossum's avatar
Guido van Rossum committed
102

Guido van Rossum's avatar
Guido van Rossum committed
103
    # If context_use_ps1 is true, parsing searches back for a ps1 line;
104
    # else searches for a popular (if, def, ...) Python stmt.
Guido van Rossum's avatar
Guido van Rossum committed
105 106
    context_use_ps1 = 0

107
    # When searching backwards for a reliable place to begin parsing,
Guido van Rossum's avatar
Guido van Rossum committed
108 109 110 111 112 113 114
    # first start num_context_lines[0] lines back, then
    # num_context_lines[1] lines back if that didn't work, and so on.
    # The last value should be huge (larger than the # of lines in a
    # conceivable file).
    # Making the initial values larger slows things down more often.
    num_context_lines = 50, 500, 5000000

Guido van Rossum's avatar
Guido van Rossum committed
115
    def __init__(self, editwin):
116
        self.editwin = editwin
Guido van Rossum's avatar
Guido van Rossum committed
117
        self.text = editwin.text
118 119 120

    def config(self, **options):
        for key, value in options.items():
121 122 123 124 125 126
            if key == 'usetabs':
                self.usetabs = value
            elif key == 'indentwidth':
                self.indentwidth = value
            elif key == 'tabwidth':
                self.tabwidth = value
Guido van Rossum's avatar
Guido van Rossum committed
127 128
            elif key == 'context_use_ps1':
                self.context_use_ps1 = value
129 130 131
            else:
                raise KeyError, "bad option name: %s" % `key`

132 133 134 135 136 137 138 139 140 141 142 143 144 145
    # If ispythonsource and guess are true, guess a good value for
    # indentwidth based on file content (if possible), and if
    # indentwidth != tabwidth set usetabs false.
    # In any case, adjust the Text widget's view of what a tab
    # character means.

    def set_indentation_params(self, ispythonsource, guess=1):
        if guess and ispythonsource:
            i = self.guess_indent()
            if 2 <= i <= 8:
                self.indentwidth = i
            if self.indentwidth != self.tabwidth:
                self.usetabs = 0

146
        self.editwin.set_tabwidth(self.tabwidth)
147

148 149
    def smart_backspace_event(self, event):
        text = self.text
150
        first, last = self.editwin.get_selection_indices()
151 152 153 154
        if first and last:
            text.delete(first, last)
            text.mark_set("insert", first)
            return "break"
155 156
        # Delete whitespace left, until hitting a real char or closest
        # preceding virtual tab stop.
157
        chars = text.get("insert linestart", "insert")
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
        if chars == '':
            if text.compare("insert", ">", "1.0"):
                # easy: delete preceding newline
                text.delete("insert-1c")
            else:
                text.bell()     # at start of buffer
            return "break"
        if  chars[-1] not in " \t":
            # easy: delete preceding real char
            text.delete("insert-1c")
            return "break"
        # Ick.  It may require *inserting* spaces if we back up over a
        # tab character!  This is written to be clear, not fast.
        expand, tabwidth = string.expandtabs, self.tabwidth
        have = len(expand(chars, tabwidth))
        assert have > 0
        want = int((have - 1) / self.indentwidth) * self.indentwidth
        ncharsdeleted = 0
        while 1:
            chars = chars[:-1]
            ncharsdeleted = ncharsdeleted + 1
            have = len(expand(chars, tabwidth))
            if have <= want or chars[-1] not in " \t":
                break
        text.undo_block_start()
        text.delete("insert-%dc" % ncharsdeleted, "insert")
        if have < want:
            text.insert("insert", ' ' * (want - have))
        text.undo_block_stop()
187 188
        return "break"

189 190 191 192 193
    def smart_indent_event(self, event):
        # if intraline selection:
        #     delete it
        # elif multiline selection:
        #     do indent-region & return
194
        # indent one level
195
        text = self.text
196
        first, last = self.editwin.get_selection_indices()
Guido van Rossum's avatar
Guido van Rossum committed
197 198 199 200 201 202 203
        text.undo_block_start()
        try:
            if first and last:
                if index2line(first) != index2line(last):
                    return self.indent_region_event(event)
                text.delete(first, last)
                text.mark_set("insert", first)
204 205 206 207 208
            prefix = text.get("insert linestart", "insert")
            raw, effective = classifyws(prefix, self.tabwidth)
            if raw == len(prefix):
                # only whitespace to the left
                self.reindent_to(effective + self.indentwidth)
Guido van Rossum's avatar
Guido van Rossum committed
209
            else:
210 211 212 213 214 215 216 217
                if self.usetabs:
                    pad = '\t'
                else:
                    effective = len(string.expandtabs(prefix,
                                                      self.tabwidth))
                    n = self.indentwidth
                    pad = ' ' * (n - effective % n)
                text.insert("insert", pad)
Guido van Rossum's avatar
Guido van Rossum committed
218 219 220 221
            text.see("insert")
            return "break"
        finally:
            text.undo_block_stop()
222

Guido van Rossum's avatar
Guido van Rossum committed
223
    def newline_and_indent_event(self, event):
224
        text = self.text
225
        first, last = self.editwin.get_selection_indices()
Guido van Rossum's avatar
Guido van Rossum committed
226 227 228 229 230 231 232 233 234
        text.undo_block_start()
        try:
            if first and last:
                text.delete(first, last)
                text.mark_set("insert", first)
            line = text.get("insert linestart", "insert")
            i, n = 0, len(line)
            while i < n and line[i] in " \t":
                i = i+1
Guido van Rossum's avatar
Guido van Rossum committed
235 236 237 238 239
            if i == n:
                # the cursor is in or at leading indentation; just inject
                # an empty line at the start
                text.insert("insert linestart", '\n')
                return "break"
Guido van Rossum's avatar
Guido van Rossum committed
240
            indent = line[:i]
Guido van Rossum's avatar
Guido van Rossum committed
241
            # strip whitespace before insert point
Guido van Rossum's avatar
Guido van Rossum committed
242 243 244
            i = 0
            while line and line[-1] in " \t":
                line = line[:-1]
Guido van Rossum's avatar
Guido van Rossum committed
245
                i = i+1
Guido van Rossum's avatar
Guido van Rossum committed
246 247
            if i:
                text.delete("insert - %d chars" % i, "insert")
Guido van Rossum's avatar
Guido van Rossum committed
248 249 250 251
            # strip whitespace after insert point
            while text.get("insert") in " \t":
                text.delete("insert")
            # start new line
Guido van Rossum's avatar
Guido van Rossum committed
252
            text.insert("insert", '\n')
253

254 255
            # adjust indentation for continuations and block
            # open/close first need to find the last stmt
Guido van Rossum's avatar
Guido van Rossum committed
256 257 258 259
            lno = index2line(text.index('insert'))
            y = PyParse.Parser(self.indentwidth, self.tabwidth)
            for context in self.num_context_lines:
                startat = max(lno - context, 1)
260 261
                startatindex = `startat` + ".0"
                rawtext = text.get(startatindex, "insert")
Guido van Rossum's avatar
Guido van Rossum committed
262
                y.set_str(rawtext)
263 264 265
                bod = y.find_good_parse_start(
                          self.context_use_ps1,
                          self._build_char_in_string_func(startatindex))
Guido van Rossum's avatar
Guido van Rossum committed
266 267 268 269 270 271 272 273 274 275 276 277
                if bod is not None or startat == 1:
                    break
            y.set_lo(bod or 0)
            c = y.get_continuation_type()
            if c != PyParse.C_NONE:
                # The current stmt hasn't ended yet.
                if c == PyParse.C_STRING:
                    # inside a string; just mimic the current indent
                    text.insert("insert", indent)
                elif c == PyParse.C_BRACKET:
                    # line up with the first (if any) element of the
                    # last open bracket structure; else indent one
278 279
                    # level beyond the indent of the line with the
                    # last open bracket
Guido van Rossum's avatar
Guido van Rossum committed
280 281 282
                    self.reindent_to(y.compute_bracket_indent())
                elif c == PyParse.C_BACKSLASH:
                    # if more than one line in this stmt already, just
283 284 285 286
                    # mimic the current indent; else if initial line
                    # has a start on an assignment stmt, indent to
                    # beyond leftmost =; else to beyond first chunk of
                    # non-whitespace on initial line
Guido van Rossum's avatar
Guido van Rossum committed
287 288 289 290
                    if y.get_num_lines_in_stmt() > 1:
                        text.insert("insert", indent)
                    else:
                        self.reindent_to(y.compute_backslash_indent())
291
                else:
Guido van Rossum's avatar
Guido van Rossum committed
292 293 294 295
                    assert 0, "bogus continuation type " + `c`
                return "break"

            # This line starts a brand new stmt; indent relative to
296 297
            # indentation of initial line of closest preceding
            # interesting stmt.
Guido van Rossum's avatar
Guido van Rossum committed
298 299 300 301 302
            indent = y.get_base_indent_string()
            text.insert("insert", indent)
            if y.is_block_opener():
                self.smart_indent_event(event)
            elif indent and y.is_block_closer():
Guido van Rossum's avatar
Guido van Rossum committed
303 304 305
                self.smart_backspace_event(event)
            return "break"
        finally:
Guido van Rossum's avatar
Guido van Rossum committed
306
            text.see("insert")
Guido van Rossum's avatar
Guido van Rossum committed
307
            text.undo_block_stop()
308

Guido van Rossum's avatar
Guido van Rossum committed
309 310
    auto_indent = newline_and_indent_event

311 312 313 314
    # Our editwin provides a is_char_in_string function that works
    # with a Tk text index, but PyParse only knows about offsets into
    # a string. This builds a function for PyParse that accepts an
    # offset.
315 316 317 318 319 320 321

    def _build_char_in_string_func(self, startindex):
        def inner(offset, _startindex=startindex,
                  _icis=self.editwin.is_char_in_string):
            return _icis(_startindex + "+%dc" % offset)
        return inner

Guido van Rossum's avatar
Guido van Rossum committed
322 323
    def indent_region_event(self, event):
        head, tail, chars, lines = self.get_region()
324 325 326
        for pos in range(len(lines)):
            line = lines[pos]
            if line:
327 328 329
                raw, effective = classifyws(line, self.tabwidth)
                effective = effective + self.indentwidth
                lines[pos] = self._make_blanks(effective) + line[raw:]
Guido van Rossum's avatar
Guido van Rossum committed
330
        self.set_region(head, tail, chars, lines)
331 332
        return "break"

Guido van Rossum's avatar
Guido van Rossum committed
333 334
    def dedent_region_event(self, event):
        head, tail, chars, lines = self.get_region()
335 336 337
        for pos in range(len(lines)):
            line = lines[pos]
            if line:
338 339 340
                raw, effective = classifyws(line, self.tabwidth)
                effective = max(effective - self.indentwidth, 0)
                lines[pos] = self._make_blanks(effective) + line[raw:]
Guido van Rossum's avatar
Guido van Rossum committed
341
        self.set_region(head, tail, chars, lines)
342 343
        return "break"

Guido van Rossum's avatar
Guido van Rossum committed
344 345
    def comment_region_event(self, event):
        head, tail, chars, lines = self.get_region()
346
        for pos in range(len(lines) - 1):
347
            line = lines[pos]
Guido van Rossum's avatar
Guido van Rossum committed
348
            lines[pos] = '##' + line
Guido van Rossum's avatar
Guido van Rossum committed
349
        self.set_region(head, tail, chars, lines)
350

Guido van Rossum's avatar
Guido van Rossum committed
351 352
    def uncomment_region_event(self, event):
        head, tail, chars, lines = self.get_region()
353 354 355 356 357 358 359 360 361
        for pos in range(len(lines)):
            line = lines[pos]
            if not line:
                continue
            if line[:2] == '##':
                line = line[2:]
            elif line[:1] == '#':
                line = line[1:]
            lines[pos] = line
Guido van Rossum's avatar
Guido van Rossum committed
362
        self.set_region(head, tail, chars, lines)
363

Guido van Rossum's avatar
Guido van Rossum committed
364 365
    def tabify_region_event(self, event):
        head, tail, chars, lines = self.get_region()
366
        tabwidth = self._asktabwidth()
367 368 369
        for pos in range(len(lines)):
            line = lines[pos]
            if line:
370 371
                raw, effective = classifyws(line, tabwidth)
                ntabs, nspaces = divmod(effective, tabwidth)
372
                lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
Guido van Rossum's avatar
Guido van Rossum committed
373 374 375 376
        self.set_region(head, tail, chars, lines)

    def untabify_region_event(self, event):
        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)
Guido van Rossum's avatar
Guido van Rossum committed
380 381
        self.set_region(head, tail, chars, lines)

382
    def toggle_tabs_event(self, event):
383
        if self.editwin.askyesno(
384
              "Toggle tabs",
385 386 387 388 389
              "Turn tabs " + ("on", "off")[self.usetabs] + "?",
              parent=self.text):
            self.usetabs = not self.usetabs
        return "break"

390
    # XXX this isn't bound to anything -- see class tabwidth comments
391
    def change_tabwidth_event(self, event):
392 393
        new = self._asktabwidth()
        if new != self.tabwidth:
394 395 396 397 398
            self.tabwidth = new
            self.set_indentation_params(0, guess=0)
        return "break"

    def change_indentwidth_event(self, event):
399
        new = self.editwin.askinteger(
400 401 402 403 404 405
                  "Indent width",
                  "New indent width (1-16)",
                  parent=self.text,
                  initialvalue=self.indentwidth,
                  minvalue=1,
                  maxvalue=16)
406 407 408 409
        if new and new != self.indentwidth:
            self.indentwidth = new
        return "break"

Guido van Rossum's avatar
Guido van Rossum committed
410
    def get_region(self):
411
        text = self.text
412 413 414 415 416
        first, last = self.editwin.get_selection_indices()
        if first and last:
            head = text.index(first + " linestart")
            tail = text.index(last + "-1c lineend +1c")
        else:
417 418 419 420 421 422
            head = text.index("insert linestart")
            tail = text.index("insert lineend +1c")
        chars = text.get(head, tail)
        lines = string.split(chars, "\n")
        return head, tail, chars, lines

Guido van Rossum's avatar
Guido van Rossum committed
423
    def set_region(self, head, tail, chars, lines):
424 425 426 427 428 429 430
        text = self.text
        newchars = string.join(lines, "\n")
        if newchars == chars:
            text.bell()
            return
        text.tag_remove("sel", "1.0", "end")
        text.mark_set("insert", head)
Guido van Rossum's avatar
Guido van Rossum committed
431
        text.undo_block_start()
432 433
        text.delete(head, tail)
        text.insert(head, newchars)
Guido van Rossum's avatar
Guido van Rossum committed
434
        text.undo_block_stop()
435
        text.tag_add("sel", head, "insert")
Guido van Rossum's avatar
Guido van Rossum committed
436

437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
    # Make string that displays as n leading blanks.

    def _make_blanks(self, n):
        if self.usetabs:
            ntabs, nspaces = divmod(n, self.tabwidth)
            return '\t' * ntabs + ' ' * nspaces
        else:
            return ' ' * n

    # Delete from beginning of line to insert point, then reinsert
    # column logical (meaning use tabs if appropriate) spaces.

    def reindent_to(self, column):
        text = self.text
        text.undo_block_start()
Guido van Rossum's avatar
Guido van Rossum committed
452 453
        if text.compare("insert linestart", "!=", "insert"):
            text.delete("insert linestart", "insert")
454 455 456 457
        if column:
            text.insert("insert", self._make_blanks(column))
        text.undo_block_stop()

458
    def _asktabwidth(self):
459
        return self.editwin.askinteger(
460 461 462 463 464 465 466
            "Tab width",
            "Spaces per tab?",
            parent=self.text,
            initialvalue=self.tabwidth,
            minvalue=1,
            maxvalue=16) or self.tabwidth

467 468 469 470 471 472 473 474 475 476 477 478 479
    # Guess indentwidth from text content.
    # Return guessed indentwidth.  This should not be believed unless
    # it's in a reasonable range (e.g., it will be 0 if no indented
    # blocks are found).

    def guess_indent(self):
        opener, indented = IndentSearcher(self.text, self.tabwidth).run()
        if opener and indented:
            raw, indentsmall = classifyws(opener, self.tabwidth)
            raw, indentlarge = classifyws(indented, self.tabwidth)
        else:
            indentsmall = indentlarge = 0
        return indentlarge - indentsmall
480 481 482 483

# "line.col" -> line, as an int
def index2line(index):
    return int(float(index))
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553

# Look at the leading whitespace in s.
# Return pair (# of leading ws characters,
#              effective # of leading blanks after expanding
#              tabs to width tabwidth)

def classifyws(s, tabwidth):
    raw = effective = 0
    for ch in s:
        if ch == ' ':
            raw = raw + 1
            effective = effective + 1
        elif ch == '\t':
            raw = raw + 1
            effective = (effective / tabwidth + 1) * tabwidth
        else:
            break
    return raw, effective

import tokenize
_tokenize = tokenize
del tokenize

class IndentSearcher:

    # .run() chews over the Text widget, looking for a block opener
    # and the stmt following it.  Returns a pair,
    #     (line containing block opener, line containing stmt)
    # Either or both may be None.

    def __init__(self, text, tabwidth):
        self.text = text
        self.tabwidth = tabwidth
        self.i = self.finished = 0
        self.blkopenline = self.indentedline = None

    def readline(self):
        if self.finished:
            return ""
        i = self.i = self.i + 1
        mark = `i` + ".0"
        if self.text.compare(mark, ">=", "end"):
            return ""
        return self.text.get(mark, mark + " lineend+1c")

    def tokeneater(self, type, token, start, end, line,
                   INDENT=_tokenize.INDENT,
                   NAME=_tokenize.NAME,
                   OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
        if self.finished:
            pass
        elif type == NAME and token in OPENERS:
            self.blkopenline = line
        elif type == INDENT and self.blkopenline:
            self.indentedline = line
            self.finished = 1

    def run(self):
        save_tabsize = _tokenize.tabsize
        _tokenize.tabsize = self.tabwidth
        try:
            try:
                _tokenize.tokenize(self.readline, self.tokeneater)
            except _tokenize.TokenError:
                # since we cut off the tokenizer early, we can trigger
                # spurious errors
                pass
        finally:
            _tokenize.tabsize = save_tabsize
        return self.blkopenline, self.indentedline