mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 03:44:55 +00:00 
			
		
		
		
	The new version (attached) is fast enough all the time in every real module
I have <whew!>.  You can make it slow by, e.g., creating an open list with
5,000 90-character identifiers (+ trailing comma) each on its own line, then
adding an item to the end -- but that still consumes less than a second on
my P5-166.  Response time in real code appears instantaneous.
Fixed some bugs.
New feature:  when hitting ENTER and the cursor is beyond the line's leading
indentation, whitespace is removed on both sides of the cursor; before
whitespace was removed only on the left; e.g., assuming the cursor is
between the comma and the space:
def something(arg1, arg2):
                   ^ cursor to the left of here, and hit ENTER
               arg2):   # new line used to end up here
              arg2):    # but now lines up the way you expect
New hack:  AutoIndent has grown a context_use_ps1 Boolean config option,
defaulting to 0 (false) and set to 1 (only) by PyShell.  Reason:  handling
the fancy stuff requires looking backward for a parsing synch point; ps1
lines are the only sensible thing to look for in a shell window, but are a
bad thing to look for in a file window (ps1 lines show up in my module
docstrings often).  PythonWin's shell should set this true too.
Persistent problem:  strings containing def/class can still screw things up
completely.  No improvement.  Simplest workaround is on the user's head, and
consists of inserting e.g.
def _(): pass
(or any other def/class) after the end of the multiline string that's
screwing them up.  This is especially irksome because IDLE's syntax coloring
is *not* confused, so when this happens the colors don't match the
indentation behavior they see.
		
	
			
		
			
				
	
	
		
			500 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			500 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import string
 | 
						|
import re
 | 
						|
import sys
 | 
						|
 | 
						|
# Reason last stmt is continued (or C_NONE if it's not).
 | 
						|
C_NONE, C_BACKSLASH, C_STRING, C_BRACKET = range(4)
 | 
						|
 | 
						|
if 0:   # for throwaway debugging output
 | 
						|
    def dump(*stuff):
 | 
						|
        sys.__stdout__.write(string.join(map(str, stuff), " ") + "\n")
 | 
						|
 | 
						|
# Find a def or class stmt.
 | 
						|
 | 
						|
_defclassre = re.compile(r"""
 | 
						|
    ^
 | 
						|
    [ \t]*
 | 
						|
    (?:
 | 
						|
        def   [ \t]+ [a-zA-Z_]\w* [ \t]* \(
 | 
						|
    |   class [ \t]+ [a-zA-Z_]\w* [ \t]*
 | 
						|
        (?: \( .* \) )?
 | 
						|
        [ \t]* :
 | 
						|
    )
 | 
						|
""", re.VERBOSE | re.MULTILINE).search
 | 
						|
 | 
						|
# Match blank line or non-indenting comment line.
 | 
						|
 | 
						|
_junkre = re.compile(r"""
 | 
						|
    [ \t]*
 | 
						|
    (?: \# \S .* )?
 | 
						|
    \n
 | 
						|
""", re.VERBOSE).match
 | 
						|
 | 
						|
# Match any flavor of string; the terminating quote is optional
 | 
						|
# so that we're robust in the face of incomplete program text.
 | 
						|
 | 
						|
_match_stringre = re.compile(r"""
 | 
						|
    \""" [^"\\]* (?:
 | 
						|
                     (?: \\. | "(?!"") )
 | 
						|
                     [^"\\]*
 | 
						|
                 )*
 | 
						|
    (?: \""" )?
 | 
						|
 | 
						|
|   " [^"\\\n]* (?: \\. [^"\\\n]* )* "?
 | 
						|
 | 
						|
|   ''' [^'\\]* (?:
 | 
						|
                   (?: \\. | '(?!'') )
 | 
						|
                   [^'\\]*
 | 
						|
                )*
 | 
						|
    (?: ''' )?
 | 
						|
 | 
						|
|   ' [^'\\\n]* (?: \\. [^'\\\n]* )* '?
 | 
						|
""", re.VERBOSE | re.DOTALL).match
 | 
						|
 | 
						|
# Match a line that starts with something interesting;
 | 
						|
# used to find the first item of a bracket structure.
 | 
						|
 | 
						|
_itemre = re.compile(r"""
 | 
						|
    [ \t]*
 | 
						|
    [^\s#\\]    # if we match, m.end()-1 is the interesting char
 | 
						|
""", re.VERBOSE).match
 | 
						|
 | 
						|
# Match start of stmts that should be followed by a dedent.
 | 
						|
 | 
						|
_closere = re.compile(r"""
 | 
						|
    \s*
 | 
						|
    (?: return
 | 
						|
    |   break
 | 
						|
    |   continue
 | 
						|
    |   raise
 | 
						|
    |   pass
 | 
						|
    )
 | 
						|
    \b
 | 
						|
""", re.VERBOSE).match
 | 
						|
 | 
						|
# Chew up non-special chars as quickly as possible, but retaining
 | 
						|
# enough info to determine the last non-ws char seen; if match is
 | 
						|
# successful, and m.group(1) isn't None, m.end(1) less 1 is the
 | 
						|
# index of the last non-ws char matched.
 | 
						|
 | 
						|
_chew_ordinaryre = re.compile(r"""
 | 
						|
    (?: \s+
 | 
						|
    |   ( [^\s[\](){}#'"\\]+ )
 | 
						|
    )+
 | 
						|
""", re.VERBOSE).match
 | 
						|
 | 
						|
# Build translation table to map uninteresting chars to "x", open
 | 
						|
# brackets to "(", and close brackets to ")".
 | 
						|
 | 
						|
_tran = ['x'] * 256
 | 
						|
for ch in "({[":
 | 
						|
    _tran[ord(ch)] = '('
 | 
						|
for ch in ")}]":
 | 
						|
    _tran[ord(ch)] = ')'
 | 
						|
for ch in "\"'\\\n#":
 | 
						|
    _tran[ord(ch)] = ch
 | 
						|
_tran = string.join(_tran, '')
 | 
						|
del ch
 | 
						|
 | 
						|
class Parser:
 | 
						|
 | 
						|
    def __init__(self, indentwidth, tabwidth):
 | 
						|
        self.indentwidth = indentwidth
 | 
						|
        self.tabwidth = tabwidth
 | 
						|
 | 
						|
    def set_str(self, str):
 | 
						|
        assert len(str) == 0 or str[-1] == '\n'
 | 
						|
        self.str = str
 | 
						|
        self.study_level = 0
 | 
						|
 | 
						|
    # Return index of start of last (probable!) def or class stmt, or
 | 
						|
    # None if none found.  It's only probable because we can't know
 | 
						|
    # whether we're in a string without reparsing from the start of
 | 
						|
    # the file -- and that's too slow in large files for routine use.
 | 
						|
    #
 | 
						|
    # Ack, hack: in the shell window this kills us, because there's
 | 
						|
    # no way to tell the differences between output, >>> etc and
 | 
						|
    # user input.  Indeed, IDLE's first output line makes the rest
 | 
						|
    # look like it's in an unclosed paren!:
 | 
						|
    # Python 1.5.2 (#0, Apr 13 1999, ...
 | 
						|
 | 
						|
    def find_last_def_or_class(self, use_ps1, _defclassre=_defclassre):
 | 
						|
        str, pos = self.str, None
 | 
						|
        if use_ps1:
 | 
						|
            # hack for shell window
 | 
						|
            ps1 = '\n' + sys.ps1
 | 
						|
            i = string.rfind(str, ps1)
 | 
						|
            if i >= 0:
 | 
						|
                pos = i + len(ps1)
 | 
						|
                self.str = str[:pos-1] + '\n' + str[pos:]
 | 
						|
        else:
 | 
						|
            i = 0
 | 
						|
            while 1:
 | 
						|
                m = _defclassre(str, i)
 | 
						|
                if m:
 | 
						|
                    pos, i = m.span()
 | 
						|
                else:
 | 
						|
                    break
 | 
						|
        return pos
 | 
						|
 | 
						|
    # Throw away the start of the string.  Intended to be called with
 | 
						|
    # find_last_def_or_class's result.
 | 
						|
 | 
						|
    def set_lo(self, lo):
 | 
						|
        assert lo == 0 or self.str[lo-1] == '\n'
 | 
						|
        if lo > 0:
 | 
						|
            self.str = self.str[lo:]
 | 
						|
 | 
						|
    # As quickly as humanly possible <wink>, find the line numbers (0-
 | 
						|
    # based) of the non-continuation lines.
 | 
						|
    # Creates self.{goodlines, continuation}.
 | 
						|
 | 
						|
    def _study1(self, _replace=string.replace, _find=string.find):
 | 
						|
        if self.study_level >= 1:
 | 
						|
            return
 | 
						|
        self.study_level = 1
 | 
						|
 | 
						|
        # Map all uninteresting characters to "x", all open brackets
 | 
						|
        # to "(", all close brackets to ")", then collapse runs of
 | 
						|
        # uninteresting characters.  This can cut the number of chars
 | 
						|
        # by a factor of 10-40, and so greatly speed the following loop.
 | 
						|
        str = self.str
 | 
						|
        str = string.translate(str, _tran)
 | 
						|
        str = _replace(str, 'xxxxxxxx', 'x')
 | 
						|
        str = _replace(str, 'xxxx', 'x')
 | 
						|
        str = _replace(str, 'xx', 'x')
 | 
						|
        str = _replace(str, 'xx', 'x')
 | 
						|
        str = _replace(str, '\nx', '\n')
 | 
						|
        # note that replacing x\n with \n would be incorrect, because
 | 
						|
        # x may be preceded by a backslash
 | 
						|
 | 
						|
        # March over the squashed version of the program, accumulating
 | 
						|
        # the line numbers of non-continued stmts, and determining
 | 
						|
        # whether & why the last stmt is a continuation.
 | 
						|
        continuation = C_NONE
 | 
						|
        level = lno = 0     # level is nesting level; lno is line number
 | 
						|
        self.goodlines = goodlines = [0]
 | 
						|
        push_good = goodlines.append
 | 
						|
        i, n = 0, len(str)
 | 
						|
        while i < n:
 | 
						|
            ch = str[i]
 | 
						|
            i = i+1
 | 
						|
 | 
						|
            # cases are checked in decreasing order of frequency
 | 
						|
            if ch == 'x':
 | 
						|
                continue
 | 
						|
 | 
						|
            if ch == '\n':
 | 
						|
                lno = lno + 1
 | 
						|
                if level == 0:
 | 
						|
                    push_good(lno)
 | 
						|
                    # else we're in an unclosed bracket structure
 | 
						|
                continue
 | 
						|
 | 
						|
            if ch == '(':
 | 
						|
                level = level + 1
 | 
						|
                continue
 | 
						|
 | 
						|
            if ch == ')':
 | 
						|
                if level:
 | 
						|
                    level = level - 1
 | 
						|
                    # else the program is invalid, but we can't complain
 | 
						|
                continue
 | 
						|
 | 
						|
            if ch == '"' or ch == "'":
 | 
						|
                # consume the string
 | 
						|
                quote = ch
 | 
						|
                if str[i-1:i+2] == quote * 3:
 | 
						|
                    quote = quote * 3
 | 
						|
                w = len(quote) - 1
 | 
						|
                i = i+w
 | 
						|
                while i < n:
 | 
						|
                    ch = str[i]
 | 
						|
                    i = i+1
 | 
						|
 | 
						|
                    if ch == 'x':
 | 
						|
                        continue
 | 
						|
 | 
						|
                    if str[i-1:i+w] == quote:
 | 
						|
                        i = i+w
 | 
						|
                        break
 | 
						|
 | 
						|
                    if ch == '\n':
 | 
						|
                        lno = lno + 1
 | 
						|
                        if w == 0:
 | 
						|
                            # unterminated single-quoted string
 | 
						|
                            if level == 0:
 | 
						|
                                push_good(lno)
 | 
						|
                            break
 | 
						|
                        continue
 | 
						|
 | 
						|
                    if ch == '\\':
 | 
						|
                        assert i < n
 | 
						|
                        if str[i] == '\n':
 | 
						|
                            lno = lno + 1
 | 
						|
                        i = i+1
 | 
						|
                        continue
 | 
						|
 | 
						|
                    # else comment char or paren inside string
 | 
						|
 | 
						|
                else:
 | 
						|
                    # didn't break out of the loop, so we're still
 | 
						|
                    # inside a string
 | 
						|
                    continuation = C_STRING
 | 
						|
                continue    # with outer loop
 | 
						|
 | 
						|
            if ch == '#':
 | 
						|
                # consume the comment
 | 
						|
                i = _find(str, '\n', i)
 | 
						|
                assert i >= 0
 | 
						|
                continue
 | 
						|
 | 
						|
            assert ch == '\\'
 | 
						|
            assert i < n
 | 
						|
            if str[i] == '\n':
 | 
						|
                lno = lno + 1
 | 
						|
                if i+1 == n:
 | 
						|
                    continuation = C_BACKSLASH
 | 
						|
            i = i+1
 | 
						|
 | 
						|
        # The last stmt may be continued for all 3 reasons.
 | 
						|
        # String continuation takes precedence over bracket
 | 
						|
        # continuation, which beats backslash continuation.
 | 
						|
        if continuation != C_STRING and level > 0:
 | 
						|
            continuation = C_BRACKET
 | 
						|
        self.continuation = continuation
 | 
						|
 | 
						|
        # Push the final line number as a sentinel value, regardless of
 | 
						|
        # whether it's continued.
 | 
						|
        assert (continuation == C_NONE) == (goodlines[-1] == lno)
 | 
						|
        if goodlines[-1] != lno:
 | 
						|
            push_good(lno)
 | 
						|
 | 
						|
    def get_continuation_type(self):
 | 
						|
        self._study1()
 | 
						|
        return self.continuation
 | 
						|
 | 
						|
    # study1 was sufficient to determine the continuation status,
 | 
						|
    # but doing more requires looking at every character.  study2
 | 
						|
    # does this for the last interesting statement in the block.
 | 
						|
    # Creates:
 | 
						|
    #     self.stmt_start, stmt_end
 | 
						|
    #         slice indices of last interesting stmt
 | 
						|
    #     self.lastch
 | 
						|
    #         last non-whitespace character before optional trailing
 | 
						|
    #         comment
 | 
						|
    #     self.lastopenbracketpos
 | 
						|
    #         if continuation is C_BRACKET, index of last open bracket
 | 
						|
 | 
						|
    def _study2(self, _rfind=string.rfind, _find=string.find,
 | 
						|
                      _ws=string.whitespace):
 | 
						|
        if self.study_level >= 2:
 | 
						|
            return
 | 
						|
        self._study1()
 | 
						|
        self.study_level = 2
 | 
						|
 | 
						|
        # Set p and q to slice indices of last interesting stmt.
 | 
						|
        str, goodlines = self.str, self.goodlines
 | 
						|
        i = len(goodlines) - 1
 | 
						|
        p = len(str)    # index of newest line
 | 
						|
        while i:
 | 
						|
            assert p
 | 
						|
            # p is the index of the stmt at line number goodlines[i].
 | 
						|
            # Move p back to the stmt at line number goodlines[i-1].
 | 
						|
            q = p
 | 
						|
            for nothing in range(goodlines[i-1], goodlines[i]):
 | 
						|
                # tricky: sets p to 0 if no preceding newline
 | 
						|
                p = _rfind(str, '\n', 0, p-1) + 1
 | 
						|
            # The stmt str[p:q] isn't a continuation, but may be blank
 | 
						|
            # or a non-indenting comment line.
 | 
						|
            if  _junkre(str, p):
 | 
						|
                i = i-1
 | 
						|
            else:
 | 
						|
                break
 | 
						|
        if i == 0:
 | 
						|
            # nothing but junk!
 | 
						|
            assert p == 0
 | 
						|
            q = p
 | 
						|
        self.stmt_start, self.stmt_end = p, q
 | 
						|
 | 
						|
        # Analyze this stmt, to find the last open bracket (if any)
 | 
						|
        # and last interesting character (if any).
 | 
						|
        lastch = ""
 | 
						|
        stack = []  # stack of open bracket indices
 | 
						|
        push_stack = stack.append
 | 
						|
        while p < q:
 | 
						|
            # suck up all except ()[]{}'"#\\
 | 
						|
            m = _chew_ordinaryre(str, p, q)
 | 
						|
            if m:
 | 
						|
                i = m.end(1) - 1    # last non-ws (if any)
 | 
						|
                if i >= 0:
 | 
						|
                    lastch = str[i]
 | 
						|
                p = m.end()
 | 
						|
                if p >= q:
 | 
						|
                    break
 | 
						|
 | 
						|
            ch = str[p]
 | 
						|
 | 
						|
            if ch in "([{":
 | 
						|
                push_stack(p)
 | 
						|
                lastch = ch
 | 
						|
                p = p+1
 | 
						|
                continue
 | 
						|
 | 
						|
            if ch in ")]}":
 | 
						|
                if stack:
 | 
						|
                    del stack[-1]
 | 
						|
                lastch = ch
 | 
						|
                p = p+1
 | 
						|
                continue
 | 
						|
 | 
						|
            if ch == '"' or ch == "'":
 | 
						|
                # consume string
 | 
						|
                # Note that study1 did this with a Python loop, but
 | 
						|
                # we use a regexp here; the reason is speed in both
 | 
						|
                # cases; the string may be huge, but study1 pre-squashed
 | 
						|
                # strings to a couple of characters per line.  study1
 | 
						|
                # also needed to keep track of newlines, and we don't
 | 
						|
                # have to.
 | 
						|
                lastch = ch
 | 
						|
                p = _match_stringre(str, p, q).end()
 | 
						|
                continue
 | 
						|
 | 
						|
            if ch == '#':
 | 
						|
                # consume comment and trailing newline
 | 
						|
                p = _find(str, '\n', p, q) + 1
 | 
						|
                assert p > 0
 | 
						|
                continue
 | 
						|
 | 
						|
            assert ch == '\\'
 | 
						|
            p = p+1     # beyond backslash
 | 
						|
            assert p < q
 | 
						|
            if str[p] != '\n':
 | 
						|
                # the program is invalid, but can't complain
 | 
						|
                lastch = ch + str[p]
 | 
						|
            p = p+1     # beyond escaped char
 | 
						|
 | 
						|
        # end while p < q:
 | 
						|
 | 
						|
        self.lastch = lastch
 | 
						|
        if stack:
 | 
						|
            self.lastopenbracketpos = stack[-1]
 | 
						|
 | 
						|
    # Assuming continuation is C_BRACKET, return the number
 | 
						|
    # of spaces the next line should be indented.
 | 
						|
 | 
						|
    def compute_bracket_indent(self, _find=string.find):
 | 
						|
        self._study2()
 | 
						|
        assert self.continuation == C_BRACKET
 | 
						|
        j = self.lastopenbracketpos
 | 
						|
        str = self.str
 | 
						|
        n = len(str)
 | 
						|
        origi = i = string.rfind(str, '\n', 0, j) + 1
 | 
						|
        j = j+1     # one beyond open bracket
 | 
						|
        # find first list item; set i to start of its line
 | 
						|
        while j < n:
 | 
						|
            m = _itemre(str, j)
 | 
						|
            if m:
 | 
						|
                j = m.end() - 1     # index of first interesting char
 | 
						|
                extra = 0
 | 
						|
                break
 | 
						|
            else:
 | 
						|
                # this line is junk; advance to next line
 | 
						|
                i = j = _find(str, '\n', j) + 1
 | 
						|
        else:
 | 
						|
            # nothing interesting follows the bracket;
 | 
						|
            # reproduce the bracket line's indentation + a level
 | 
						|
            j = i = origi
 | 
						|
            while str[j] in " \t":
 | 
						|
                j = j+1
 | 
						|
            extra = self.indentwidth
 | 
						|
        return len(string.expandtabs(str[i:j],
 | 
						|
                                     self.tabwidth)) + extra
 | 
						|
 | 
						|
    # Return number of physical lines in last stmt (whether or not
 | 
						|
    # it's an interesting stmt!  this is intended to be called when
 | 
						|
    # continuation is C_BACKSLASH).
 | 
						|
 | 
						|
    def get_num_lines_in_stmt(self):
 | 
						|
        self._study1()
 | 
						|
        goodlines = self.goodlines
 | 
						|
        return goodlines[-1] - goodlines[-2]
 | 
						|
 | 
						|
    # Assuming continuation is C_BACKSLASH, return the number of spaces
 | 
						|
    # the next line should be indented.  Also assuming the new line is
 | 
						|
    # the first one following the initial line of the stmt.
 | 
						|
 | 
						|
    def compute_backslash_indent(self):
 | 
						|
        self._study2()
 | 
						|
        assert self.continuation == C_BACKSLASH
 | 
						|
        str = self.str
 | 
						|
        i = self.stmt_start
 | 
						|
        while str[i] in " \t":
 | 
						|
            i = i+1
 | 
						|
        startpos = i
 | 
						|
 | 
						|
        # See whether the initial line starts an assignment stmt; i.e.,
 | 
						|
        # look for an = operator
 | 
						|
        endpos = string.find(str, '\n', startpos) + 1
 | 
						|
        found = level = 0
 | 
						|
        while i < endpos:
 | 
						|
            ch = str[i]
 | 
						|
            if ch in "([{":
 | 
						|
                level = level + 1
 | 
						|
                i = i+1
 | 
						|
            elif ch in ")]}":
 | 
						|
                if level:
 | 
						|
                    level = level - 1
 | 
						|
                i = i+1
 | 
						|
            elif ch == '"' or ch == "'":
 | 
						|
                i = _match_stringre(str, i, endpos).end()
 | 
						|
            elif ch == '#':
 | 
						|
                break
 | 
						|
            elif level == 0 and ch == '=' and \
 | 
						|
                   (i == 0 or str[i-1] not in "=<>!") and \
 | 
						|
                   str[i+1] != '=':
 | 
						|
                found = 1
 | 
						|
                break
 | 
						|
            else:
 | 
						|
                i = i+1
 | 
						|
 | 
						|
        if found:
 | 
						|
            # found a legit =, but it may be the last interesting
 | 
						|
            # thing on the line
 | 
						|
            i = i+1     # move beyond the =
 | 
						|
            found = re.match(r"\s*\\", str[i:endpos]) is None
 | 
						|
 | 
						|
        if not found:
 | 
						|
            # oh well ... settle for moving beyond the first chunk
 | 
						|
            # of non-whitespace chars
 | 
						|
            i = startpos
 | 
						|
            while str[i] not in " \t\n":
 | 
						|
                i = i+1
 | 
						|
 | 
						|
        return len(string.expandtabs(str[self.stmt_start :
 | 
						|
                                         i],
 | 
						|
                                     self.tabwidth)) + 1
 | 
						|
 | 
						|
    # Return the leading whitespace on the initial line of the last
 | 
						|
    # interesting stmt.
 | 
						|
 | 
						|
    def get_base_indent_string(self):
 | 
						|
        self._study2()
 | 
						|
        i, n = self.stmt_start, self.stmt_end
 | 
						|
        j = i
 | 
						|
        str = self.str
 | 
						|
        while j < n and str[j] in " \t":
 | 
						|
            j = j + 1
 | 
						|
        return str[i:j]
 | 
						|
 | 
						|
    # Did the last interesting stmt open a block?
 | 
						|
 | 
						|
    def is_block_opener(self):
 | 
						|
        self._study2()
 | 
						|
        return self.lastch == ':'
 | 
						|
 | 
						|
    # Did the last interesting stmt close a block?
 | 
						|
 | 
						|
    def is_block_closer(self):
 | 
						|
        self._study2()
 | 
						|
        return _closere(self.str, self.stmt_start) is not None
 |