root/trunk/peppy/lib/autoindent.py

Revision 1594, 39.8 kB (checked in by rob, 8 days ago)

Fixed #519: added isInsideStatement method to check whether or not semicolon is in an incomplete statement
* fixed off-by-one error in getFoldSectionStart
* added unit tests for isInsideStatement

Line 
1# peppy Copyright (c) 2006-2008 Rob McMullen
2# Licenced under the GPLv2; see http://peppy.flipturn.org for more info
3
4# Transcribed from KDE's kateautoindent.cpp which was licensed under the
5# LGPLv2.  LGPL code can be relicensed under the GPL according to LGPL section
6# 3, which is what I've done here.  The original kate code contained the
7# following copyright:
8#   Copyright (C) 2003 Jesse Yurkovich <yurkjes@iit.edu>
9#   Copyright (C) 2004 >Anders Lund <anders@alweb.dk> (KateVarIndent class)
10#   Copyright (C) 2005 Dominik Haumann <dhdev@gmx.de> (basic support for config page)
11
12"""Autoindent code for peppy
13
14This is a collection of autoindent code designed for use with peppy's
15enhancements to the wx.StyledTextCtrl.
16
17If you wanted to use this without peppy, you'd need to provide several
18methods, including C{isStyleComment}, C{isStyleString}, C{getLinesep},
19(and others) to the stc of interest.  See the implementation of them in
20L{peppy.editra.stcmixin} and L{peppy.stcbase}.
21"""
22
23import os, re
24from cStringIO import StringIO
25
26import wx.stc
27from peppy.debug import *
28
29
30class BasicAutoindent(debugmixin):
31    """Simple autoindent that indents the line to the level of the line above it.
32   
33    This is about the bare minimum indenter.  It just looks at the line above
34    it and reports the indent level of that line.  No effort is made to look
35    at syntax or anything.  Simple.
36    """
37   
38    # Scintilla always reports a fold of zero on the last line, so if
39    # subclasses use folding to determine the indent, this should be set to
40    # True so that processReturn will add an extra newline character if it's
41    # at the end of the file.
42    folding_last_line_bug = False
43   
44    def __init__(self):
45        pass
46   
47    def findIndent(self, stc, linenum):
48        """Find proper indention of current line based on the previous line
49
50        This is designed to be overridden in subclasses.  Given the current
51        line and assuming the current line is indented correctly, figure out
52        what the indention should be for the next line.
53       
54        @param linenum: line number
55        @return: integer indicating number of columns to indent the following
56        line
57        """
58        # look at indention of previous line
59        prevind, prevline = stc.GetPrevLineIndentation(linenum)
60        #if (prevind < indcol and prevline < linenum-1) or prevline < linenum-2:
61        #    # if there's blank lines before this and the previous
62        #    # non-blank line is indented less than this one, ignore
63        #    # it.  Make the user manually unindent lines.
64        #    return None
65
66        # previous line is not blank, so indent line to previous
67        # line's level
68        return prevind
69
70    def reindentLine(self, stc, linenum=None, dedent_only=False):
71        """Reindent the specified line to the correct level.
72
73        Changes the indentation of the given line by inserting or deleting
74        whitespace as required.  This operation is typically bound to the tab
75        key, but regardless to the actual keypress to which it is bound is
76        *only* called in response to a user keypress.
77       
78        @param stc: the stc of interest
79        @param linenum: the line number, or None to use the current line
80        @param dedent_only: flag to indicate that indentation should only be
81        removed, not added
82        @return: the new cursor position, in case the cursor has moved as a
83        result of the indention.
84        """
85        if linenum is None:
86            linenum = stc.GetCurrentLine()
87        if linenum == 0:
88            # first line is always indented correctly
89            return stc.GetCurrentPos()
90       
91        linestart = stc.PositionFromLine(linenum)
92
93        # actual indention of current line
94        indcol = stc.GetLineIndentation(linenum) # columns
95        pos = stc.GetCurrentPos()
96        indpos = stc.GetLineIndentPosition(linenum) # absolute character position
97        col = stc.GetColumn(pos)
98        self.dprint("linestart=%d indpos=%d pos=%d col=%d indcol=%d" % (linestart, indpos, pos, col, indcol))
99
100        newind = self.findIndent(stc, linenum)
101        if newind is None:
102            return pos
103        if dedent_only and newind > indcol:
104            return pos
105           
106        # the target to be replaced is the leading indention of the
107        # current line
108        indstr = stc.GetIndentString(newind)
109        self.dprint("linenum=%d indstr='%s'" % (linenum, indstr))
110        stc.SetTargetStart(linestart)
111        stc.SetTargetEnd(indpos)
112        stc.ReplaceTarget(indstr)
113
114        # recalculate cursor position, because it may have moved if it
115        # was within the target
116        after = stc.GetLineIndentPosition(linenum)
117        self.dprint("after: indent=%d cursor=%d" % (after, stc.GetCurrentPos()))
118        if pos < linestart:
119            return pos
120        newpos = pos - indpos + after
121        if newpos < linestart:
122            # we were in the indent region, but the region was made smaller
123            return after
124        elif pos < indpos:
125            # in the indent region
126            return after
127        return newpos
128
129    def processReturn(self, stc):
130        """Add a newline and indent to the proper tab level.
131
132        Indent to the level of the line above.  This uses the findIndent method
133        to determine the proper indentation of the line about to be added,
134        inserts the appropriate end-of-line characters, and indents the new
135        line to that indentation level.
136       
137        @param stc: stc of interest
138        """
139        linesep = stc.getLinesep()
140       
141        stc.BeginUndoAction()
142        # reindent current line (if necessary), then process the return
143        #pos = stc.reindentLine()
144       
145        linenum = stc.GetCurrentLine()
146        pos = stc.GetCurrentPos()
147        col = stc.GetColumn(pos)
148        #linestart = stc.PositionFromLine(linenum)
149        #line = stc.GetLine(linenum)[:pos-linestart]
150   
151        #get info about the current line's indentation
152        ind = stc.GetLineIndentation(linenum)
153
154        self.dprint("format = %s col=%d ind = %d" % (repr(linesep), col, ind))
155
156        stc.SetTargetStart(pos)
157        stc.SetTargetEnd(pos)
158        if col <= ind:
159            newline = linesep + stc.GetIndentString(col)
160        elif not pos:
161            newline = linesep
162        else:
163            stc.ReplaceTarget(linesep)
164            pos += len(linesep)
165            end = min(pos + 1, stc.GetTextLength())
166           
167            # Scintilla always returns a fold level of zero on the last line,
168            # so when trying to indent the last line, must add a newline
169            # character.
170            if pos == end and self.folding_last_line_bug:
171                stc.AddText("\n")
172                end = stc.GetTextLength()
173           
174            # When we insert a new line, the colorization isn't always
175            # immediately updated, so we have to force that here before
176            # calling findIndent to guarantee that the new line will have the
177            # correct fold property set.
178            stc.Colourise(stc.PositionFromLine(linenum), end)
179            stc.SetTargetStart(pos)
180            stc.SetTargetEnd(pos)
181            ind = self.findIndent(stc, linenum + 1)
182            self.dprint("pos=%d ind=%d fold=%d" % (pos, ind, (stc.GetFoldLevel(linenum+1)&wx.stc.STC_FOLDLEVELNUMBERMASK) - wx.stc.STC_FOLDLEVELBASE))
183            newline = stc.GetIndentString(ind)
184        stc.ReplaceTarget(newline)
185        stc.GotoPos(pos + len(newline))
186        stc.EndUndoAction()
187
188    def processTab(self, stc):
189        stc.BeginUndoAction()
190        self.dprint()
191        pos = self.reindentLine(stc)
192        stc.GotoPos(pos)
193        stc.EndUndoAction()
194   
195    def electricChar(self, stc, uchar):
196        """Autoindent in response to a special character
197
198        This is a hook to cause an autoindent on a particular character.
199        Note that the hook can do more than that -- it can insert or delete
200        characters as well.
201       
202        This takes its name from emacs, where "electric" meant that something
203        else happened other than simply inserting the char.
204       
205        @param stc: stc instance
206       
207        @param uchar: unicode character that was just typed by the user (note
208        that it hasn't been inserted into the document yet.)
209       
210        @return: True if this method handled the character and the text
211        was modified; False if the calling event handler should handle the
212        character.
213        """
214        return False
215   
216    def electricDelete(self, stc):
217        """Delete the next character
218       
219        This hook allows more complex processing of the Delete key than the
220        default which is to delete the character after the cursor.
221        """
222        stc.CmdKeyExecute(wx.stc.STC_CMD_CLEAR)
223   
224    def electricBackspace(self, stc):
225        """Delete the previous character
226       
227        This hook allows more complex processing of the Backspace key than the
228        default which is to delete the character before the cursor.
229        """
230        stc.CmdKeyExecute(wx.stc.STC_CMD_DELETEBACK)
231
232
233class FoldingAutoindent(BasicAutoindent):
234    """Experimental class to use STC Folding to reindent a line.
235    """
236    folding_last_line_bug = True
237   
238    def getFold(self, stc, linenum):
239        return stc.GetFoldLevel(linenum)&wx.stc.STC_FOLDLEVELNUMBERMASK - wx.stc.STC_FOLDLEVELBASE
240
241    def getPreviousText(self, stc, linenum):
242        """Find the text above the line with the same fold level.
243       
244        """
245        fold = self.getFold(stc, linenum)
246        ln = linenum - 1
247        fc = lc = stc.GetLineEndPosition(ln)
248        above = ''
249        while ln > 0:
250            f = self.getFold(stc, ln)
251            if f != fold:
252                above = stc.GetTextRange(fc, lc)
253                break
254            fc = stc.PositionFromLine(ln)
255            ln -= 1
256        return above
257
258    def getFoldSectionStart(self, stc, linenum):
259        """Find the line number of the text above the given line that has the
260        same fold level.
261       
262        Searching from the given line number toward the start of the file,
263        find the set of lines that have the same fold level as the given
264        line.  Once the fold level changes, we know that we're done searching
265        because there's no way that any indentation for the given line can be
266        affected by a separate block of code
267
268        @return: the line number of the start of the fold section
269        """
270        fold = self.getFold(stc, linenum)
271        ln = linenum
272        while ln > 0:
273            f = self.getFold(stc, ln - 1)
274            if f != fold:
275                break
276            ln -= 1
277        return ln
278   
279    def getNonCodeStyles(self, stc):
280        """Return a list of styling integers that indicate characters that are
281        outside the normal code flow.
282       
283        Character styles that are outside code flow are strings, comments,
284        preprocessor statements, and stuff that should be ignored when
285        indenting.
286        """
287        styles = []
288        styles.extend(stc.getStringStyles())
289        styles.extend(stc.getCommentStyles())
290        #self.dprint(styles)
291        return styles
292   
293    def getCodeChars(self, stc, ln, lc=-1):
294        """Get a version of the given line with all non code chars blanked out.
295       
296        This function blanks out all non-code characters (comments, strings,
297        etc) from the line and returns a copy of the interesting stuff.
298       
299        @param stc: stc of interest
300        @param ln: line number
301        @param lc: optional integer specifying the last position on the
302        line to consider
303        """
304        fc = stc.PositionFromLine(ln)
305        if lc < 0:
306            lc = stc.GetLineEndPosition(ln)
307        if lc < fc:
308            # FIXME: fail hard during development
309            raise IndexError("bad line specification for line %d: fc=%d lc=%d" % (ln, fc, lc))
310       
311        mask = (2 ** stc.GetStyleBits()) - 1
312       
313        out = []
314
315        line = stc.GetStyledText(fc, lc)
316        self.dprint(repr(line))
317        i = len(line)
318       
319        # replace all uninteresting chars with blanks
320        skip = self.getNonCodeStyles(stc)
321        while i > 0:
322            i -= 1
323            s = ord(line[i]) & mask
324            i -= 1
325            if s in skip:
326                c = ' '
327            else:
328                c = line[i]
329            out.append(c)
330       
331        # Note that we assembled the string in reverse, so flip it around
332        out = ''.join(reversed(out))
333        return out
334
335    def getLastNonWhitespaceChar(self, stc, pos):
336        """Working backward, find the closest non-whitespace character
337       
338        @param stc: stc of interest
339        @param pos: boundary position from which to start looking backwards
340        @return: tuple of the matching char and the position
341        """
342        found = ''
343        skip = self.getNonCodeStyles(stc)
344        while pos > 0:
345            check = pos - 1
346            c = unichr(stc.GetCharAt(check))
347            s = stc.GetStyleAt(check)
348            #dprint("check=%d char='%s'" % (check, c))
349           
350            # Comment or string terminates the search and will return with the
351            # character after the last comment/string char.
352            if s in skip:
353                break
354           
355            found = c
356            pos = check
357            if not c.isspace():
358                break
359        return (found, pos)
360
361    def getBraceMatch(self, text, open=u'(', close=u')'):
362        """Search the text to see if there are unmatched braces
363       
364        Search the line for unmatched braces given the open and close matching
365        pair.  This does not look at styling information; it assumes that
366        L{getCodeChars} has been called before this.
367       
368        @param text: line number
369        @param open: the opening brace character, e.g. "("
370        @param close: the complimentary closing brace character, e.g. ")"
371       
372        @return: brace mismatch count: 0 for matching braces, positive for a
373        surplus of opening braces, and negative for a surplus of closing braces
374        """
375        r = 0
376        for c in text:
377            if c == open:
378                r += 1
379            elif c == close:
380                r -= 1
381        return r
382
383    def findIndent(self, stc, linenum=None):
384        """Reindent the specified line to the correct level.
385
386        Given a line, use Scintilla's built-in folding to determine
387        the indention level of the current line.
388        """
389        if linenum is None:
390            linenum = stc.GetCurrentLine()
391        linestart = stc.PositionFromLine(linenum)
392
393        # actual indention of current line
394        ind = stc.GetLineIndentation(linenum) # columns
395        pos = stc.GetLineIndentPosition(linenum) # absolute character position
396
397        # folding says this should be the current indention
398        fold = stc.GetFoldLevel(linenum)&wx.stc.STC_FOLDLEVELNUMBERMASK - wx.stc.STC_FOLDLEVELBASE
399        self.dprint("ind = %s (char num=%d), fold = %s" % (ind, pos, fold))
400        return fold * stc.GetIndent()
401
402
403class CStyleAutoindent(FoldingAutoindent):
404    """Use the STC Folding to reindent a line in a C-like mode.
405   
406    This autoindenter uses the built-in Scintilla folding to determine the
407    correct indent level for C-like modes (C, C++, Java, Javascript, etc.)
408    plus a bunch of heuristics to handle things that Scintilla doesn't.
409    """
410    debuglevel = 0
411   
412    def __init__(self, reIndentAfter=None, reIndent=None, reUnindent=None):
413        """Create a regex autoindenter.
414       
415        Creates an instance of the regex autoindenter.  Since this code is
416        based on the varindent module from KDE's Kate editor, it uses the same
417        regular expressions as Kate to indent code.
418       
419        @param reIndentAfter: a regular expression used on the nearest line
420        above the current line that has content.  If it matches, it will add
421        indentation to the current line
422       
423        @param reIndent: regular expression used on the current line.  If it
424        matches, indentation will be added to the current line.
425       
426        @param reUnindent: regular expression used on the current line.  If it
427        matches, indentation is removed from the current line.
428       
429        @param braces: a list of braces used on the nearest line with content
430        above the current.  When an opening brace appears without its matching
431        closing brace, and indentation level is added to the current line.
432        """
433        if reIndentAfter:
434            self.reIndentAfter = re.compile(reIndentAfter)
435        else:
436            self.reIndentAfter = re.compile(r'^(?!.*;\s*//).*[^\s;{}]\s*$')
437        if reIndent:
438            self.reIndent = re.compile(reIndent)
439        else:
440            self.reIndent = None
441        if reUnindent:
442            self.reUnindent = re.compile(reUnindent)
443        else:
444            self.reUnindent = None
445        self.reStatement = re.compile(r'^([^\s]+)\s*(\(.+\)|.+)?$')
446        self.reCase = re.compile(r'^\s*(case|default).*:$')
447        self.reClassAttrScope = re.compile(r'^\s*(public|private).*:$')
448        self.reBreak = re.compile(r'^\s*break\s*;\s*$')
449        self.reLabel = re.compile(r'^\s*[a-zA-Z_][a-zA-Z0-9_]*:((?!:)|$)')
450   
451    def getNonCodeStyles(self, stc):
452        """Return a list of styling integers that indicate characters that are
453        outside the normal code flow.
454       
455        Character styles that are outside code flow are strings, comments,
456        preprocessor statements, and stuff that should be ignored when
457        indenting.
458        """
459        styles = []
460        styles.extend(stc.getStringStyles())
461        styles.extend(stc.getCommentStyles())
462        # add preprocessor styles for C-like modes -- everything that uses the
463        # C syntax highlighter should also use the STC_C_PREPROCESSOR token
464        styles.append(wx.stc.STC_C_PREPROCESSOR)
465        #self.dprint(styles)
466        return styles
467   
468    def getBraceOpener(self, stc, linenum):
469        """Find the statement related to the brace's opening.
470       
471        If a block of code is related to a reserved keyword, like
472       
473        switch (blah) {
474            case 0:
475                stuff;
476        }
477       
478        return the keyword so it can be used to further indent the contents.
479
480        @return: the keyword of interest
481        """
482        fold = self.getFold(stc, linenum)
483        self.dprint("linenum=%d fold=%d" % (linenum, fold))
484        ln = linenum
485        statement = ''
486        first = True
487        parens = False
488        while ln >= 0:
489            f = self.getFold(stc, ln)
490            if f != fold:
491                break
492            line = self.getCodeChars(stc, ln).strip()
493            self.dprint(line)
494            ln -= 1
495            if not line:
496                continue
497            statement = line + statement
498            if first:
499                if statement.endswith('{'):
500                    statement = statement[:-1].strip()
501                   
502                if statement.endswith(';'):
503                    # A complete statement before an opening brace means it's
504                    # an anonymous block, so the statement before doesn't
505                    # relate to the block itself.
506                    break
507                if ')' in statement:
508                    parens = True
509                else:
510                    break
511                first = False
512           
513            # Parens must match to form a complete statement
514            if parens:
515                if self.getBraceMatch(statement) == 0:
516                    break
517        if statement:
518            match = self.reStatement.match(statement)
519            if match:
520                statement = match.group(1)
521        return statement
522   
523    def isInsideStatement(self, stc, pos):
524        """Find out if the position is inside a statement
525       
526        Being inside a statement means that the position is after an opening
527        paren and the matching close paran either doesn't exist yet or is
528        after the position.
529       
530        @return: True if pos is after an unbalanced amount of opening parens
531        """
532        linenum = stc.LineFromPosition(pos)
533
534        start = self.getFoldSectionStart(stc, linenum)
535        self.dprint("fold start=%d, linenum=%d" % (start, linenum))
536        text = self.getCodeChars(stc, start, pos)
537        parens = self.getBraceMatch(text)
538        self.dprint("text=%s, parens=%d" % (text, parens))
539        return parens != 0
540   
541    def findIndent(self, stc, linenum=None):
542        """Reindent the specified line to the correct level.
543
544        Given a line, use Scintilla's built-in folding to determine
545        the indention level of the current line.
546        """
547        if linenum is None:
548            linenum = stc.GetCurrentLine()
549        linestart = stc.PositionFromLine(linenum)
550
551        # actual indention of current line
552        col = stc.GetLineIndentation(linenum) # columns
553        pos = stc.GetLineIndentPosition(linenum) # absolute character position
554
555        # folding says this should be the current indention
556        fold = (stc.GetFoldLevel(linenum)&wx.stc.STC_FOLDLEVELNUMBERMASK) - wx.stc.STC_FOLDLEVELBASE
557        c = stc.GetCharAt(pos)
558        s = stc.GetStyleAt(pos)
559        indent = stc.GetIndent()
560        partial = 0
561        self.dprint("col=%d (pos=%d), fold=%d char=%s" % (col, pos, fold, repr(chr(c))))
562        if c == ord('}'):
563            # Scintilla doesn't automatically dedent the closing brace, so we
564            # force that here.
565            fold -= 1
566        elif c == ord('{'):
567            # Opening brace on a line by itself always stays at the fold level
568            pass
569        elif c == ord('#') and s == 9:
570            # Force preprocessor directives to start at column zero
571            fold = 0
572        else:
573            start = self.getFoldSectionStart(stc, linenum)
574            opener = self.getBraceOpener(stc, start-1)
575            self.dprint(opener)
576           
577            # First, try to match on the current line to see if we know enough
578            # about it to figure its indent level
579            matched = False
580            line = self.getCodeChars(stc, linenum)
581            if opener == "switch":
582                # case statements are partially dedented relative to the
583                # scintilla level
584                if self.reCase.match(line):
585                    matched = True
586                    partial = - (indent / 2)
587            elif opener == "class":
588                # public/private/protected statements are partially dedented
589                # relative to the scintilla level
590                if self.reClassAttrScope.match(line):
591                    matched = True
592                    partial = - (indent / 2)
593           
594            # labels are matched after case statements to prevent the
595            # 'default:' label from getting confused with a regular label
596            if not matched and self.reLabel.match(line):
597                fold = 0
598                matched = True
599           
600            # If we can't determine the indent level when only looking at
601            # the current line, start backing up to find the first non blank
602            # statement above the line.  We then look to see if we should
603            # indent relative to that statement (e.g.  if the statement is a
604            # continuation) or relative to the fold level supplied by scintilla
605            if not matched:
606                for ln in xrange(linenum - 1, start - 1, -1):
607                    line = self.getCodeChars(stc, ln)
608                    self.dprint(line)
609                    if not line.strip() or self.reLabel.match(line):
610                        continue
611                    if opener == "switch":
612                        if self.reCase.match(line):
613                            # a case statement will be interpreted as a continuation
614                            break
615                    if self.reIndentAfter.match(line):
616                        self.dprint("continuation")
617                        fold += 1
618                    else:
619                        self.dprint("terminated statement")
620                    break
621
622        return (fold * indent) + partial
623   
624    def electricChar(self, stc, uchar):
625        """Reindent the line and insert a newline when special chars are typed.
626       
627        Like emacs, a semicolon or curly brace causes the line to be reindented
628        and the next line to be indented to the correct column.
629       
630        @param stc: stc instance
631       
632        @param uchar: unicode character that was just typed by the user (note
633        that it hasn't been inserted into the document yet.)
634       
635        @return: True if this method handled the character and the text
636        was modified; False if the calling event handler should handle the
637        character.
638        """
639        implicit_return = True
640       
641        if uchar == u';' or uchar == u':' or uchar == '{' or uchar == '}':
642            pos = stc.GetCurrentPos()
643            s = stc.GetStyleAt(pos)
644            if not stc.isStyleComment(s) and not stc.isStyleString(s):
645                if uchar == u':':
646                    # FIXME: currently only process the : if the current
647                    # line is a case statement.  Emacs also indents labels
648                    # and namespace operators with a :: by checking if the
649                    # last character on the previous line is a : and if so
650                    # collapses the current line with the previous line and
651                    # reindents the new line
652                    linenum = stc.GetCurrentLine()
653                    line = self.getCodeChars(stc, linenum, pos) + ":"
654                    if not self.reCase.match(line) and not self.reClassAttrScope.match(line):
655                        c, prev = self.getLastNonWhitespaceChar(stc, pos)
656                        if c == u':':
657                            # Found previous ':', so make it a double colon
658                            stc.SetSelection(prev + 1, pos)
659                            #dprint("selection: %d - %d" % (prev + 1, pos))
660                        implicit_return = False
661                elif uchar == u';':
662                    # Don't process the semicolon if we're in the middle of an
663                    # open statement
664                    if self.isInsideStatement(stc, pos):
665                        return False
666                stc.BeginUndoAction()
667                start, end = stc.GetSelection()
668                if start == end:
669                    stc.AddText(uchar)
670                else:
671                    stc.ReplaceSelection(uchar)
672               
673                # Always reindent the line, but only process a return if needed
674                self.processTab(stc)
675                if implicit_return:
676                    self.processReturn(stc)
677               
678                stc.EndUndoAction()
679                return True
680        return False
681
682    def electricDelete(self, stc):
683        """Delete all whitespace after the cursor unless in a string or comment
684        """
685        start, end = stc.GetSelection()
686        if start != end:
687            stc.ReplaceSelection("")
688            return
689        pos = stc.GetCurrentPos()
690        s = stc.GetStyleAt(pos)
691        if stc.isStyleComment(s) or stc.isStyleString(s):
692            stc.CmdKeyExecute(wx.stc.STC_CMD_CLEAR)
693        else:
694            self.dprint("deleting from pos %d" % pos)
695            end = pos
696            while end < stc.GetLength():
697                c = stc.GetCharAt(end)
698                if c == ord(' ') or c == ord('\t') or c == 10 or c == 13:
699                    end += 1
700                else:
701                    break
702            if end > pos:
703                stc.SetTargetStart(pos)
704                stc.SetTargetEnd(end)
705                stc.ReplaceTarget('')
706            else:
707                stc.CmdKeyExecute(wx.stc.STC_CMD_CLEAR)
708
709    def electricBackspace(self, stc):
710        """Delete all whitespace before the cursor unless in a string or comment
711        """
712        start, end = stc.GetSelection()
713        if start != end:
714            stc.ReplaceSelection("")
715            return
716        pos = stc.GetCurrentPos()
717        if pos <= 0:
718            return
719        s = stc.GetStyleAt(pos - 1)
720        if stc.isStyleComment(s) or stc.isStyleString(s):
721            stc.CmdKeyExecute(wx.stc.STC_CMD_DELETEBACK)
722        else:
723            self.dprint("backspace from pos %d" % pos)
724            start = pos
725            while start > 0:
726                c = stc.GetCharAt(start - 1)
727                if c == ord(' ') or c == ord('\t') or c == 10 or c == 13:
728                    start -= 1
729                else:
730                    break
731            if start < pos:
732                stc.SetTargetStart(start)
733                stc.SetTargetEnd(pos)
734                stc.ReplaceTarget('')
735            else:
736                stc.CmdKeyExecute(wx.stc.STC_CMD_DELETEBACK)
737
738
739
740class RegexAutoindent(BasicAutoindent):
741    """Regex based autoindenter
742
743    This is a flexible autointent module that uses regular expressions to
744    determine the proper indentation level of a line of code based on the line
745    of code above it.
746
747    The original module was written by Anders Lund for
748    the kate editor of the KDE project.  He has a U{blog
749    entry<http://www.alweb.dk/blog/anders/autoindentation>} describing his
750    thinking that led up to the creation of the code.
751   
752    The source code for the autoindenting part of Kate is actually in the
753    L{kpart<http://websvn.kde.org/branches/KDE/3.5/kdelibs/kate/part/kateautoindent.cpp?revision=620408&view=markup>}
754    section of KDE, not with the main Kate application.
755    """
756   
757    def __init__(self, reIndentAfter, reIndent, reUnindent, braces):
758        """Create a regex autoindenter.
759       
760        Creates an instance of the regex autoindenter.  Since this code is
761        based on the varindent module from KDE's Kate editor, it uses the same
762        regular expressions as Kate to indent code.
763       
764        @param reIndentAfter: a regular expression used on the nearest line
765        above the current line that has content.  If it matches, it will add
766        indentation to the current line
767       
768        @param reIndent: regular expression used on the current line.  If it
769        matches, indentation will be added to the current line.
770       
771        @param reUnindent: regular expression used on the current line.  If it
772        matches, indentation is removed from the current line.
773       
774        @param braces: a list of braces used on the nearest line with content
775        above the current.  When an opening brace appears without its matching
776        closing brace, and indentation level is added to the current line.
777        """
778        if reIndentAfter:
779            self.reIndentAfter = re.compile(reIndentAfter)
780        else:
781            self.reIndentAfter = None
782        if reIndent:
783            self.reIndent = re.compile(reIndent)
784        else:
785            self.reIndent = None
786        if reUnindent:
787            self.reUnindent = re.compile(reUnindent)
788        else:
789            self.reUnindent = None
790       
791        # list of brace types to handle.  Should be the opening brace, e.g.
792        # '(', '[', or '{'
793        if braces:
794            self.couples = braces.replace(')', '(').replace(']', '[').replace('}', '{')
795        else:
796            self.couples = ''
797
798    def findIndent(self, stc, linenum):
799        """Determine the correct indentation for the line.
800       
801        This routine uses regular expressions to determine the indentation
802        level of the line.
803       
804        @param linenum: current line number
805       
806        @param return: the number of columns to indent, or None to leave as-is
807        """
808        if linenum < 1:
809            return None
810
811        #// find the first line with content that is not starting with comment text,
812        #// and take the position from that
813        ln = linenum
814        pos = 0
815        above = ''
816        while ln > 0:
817            ln -= 1
818            fc = stc.GetLineIndentPosition(ln)
819            lc = stc.GetLineEndPosition(ln)
820            self.dprint("ln=%d fc=%d lc=%d line=-->%s<--" % (ln, fc, lc, stc.GetLine(ln)))
821            # skip blank lines
822            if fc < lc:
823                s = stc.GetStyleAt(fc)
824                if stc.isStyleComment(s):
825                    continue
826                pos = stc.GetLineIndentation(ln)
827                above = stc.GetTextRange(fc, lc)
828                break
829
830        #  // try 'couples' for an opening on the above line first. since we only adjust by 1 unit,
831        #  // we only need 1 match.
832        adjustment = 0
833        if '(' in self.couples and self.coupleBalance(stc, ln, '(', ')') > 0:
834            adjustment += 1
835        elif '[' in self.couples and self.coupleBalance(stc, ln, '[', ']') > 0:
836            adjustment += 1
837        elif '{' in self.couples and self.coupleBalance(stc, ln, '{', '}') > 0:
838            adjustment += 1
839       
840        #  // Try 'couples' for a closing on this line first. since we only adjust by 1 unit,
841        #  // we only need 1 match. For unindenting, we look for a closing character
842        #  // *at the beginning of the line*
843        #  // NOTE Assume that a closing brace with the configured attribute on the start
844        #  // of the line is closing.
845        #  // When acting on processChar, the character isn't highlighted. So I could
846        #  // either not check, assuming that the first char *is* meant to close, or do a
847        #  // match test if the attrib is 0. How ever, doing that is
848        #  // a potentially huge job, if the match is several hundred lines away.
849        #  // Currently, the check is done.
850        #  {
851        #    KateTextLine::Ptr tl = doc->plainKateTextLine( line.line() );
852        #    int i = tl->firstChar();
853        #    if ( i > -1 )
854        #    {
855        #      QChar ch = tl->getChar( i );
856        #      uchar at = tl->attribute( i );
857        #      kdDebug(13030)<<"attrib is "<<at<<endl;
858        #      if ( d->couples & Parens && ch == ')'
859        #           && ( at == d->coupleAttrib
860        #                || (! at && hasRelevantOpening( KateDocCursor( line.line(), i, doc ) ))
861        #              )
862        #         )
863        #        adjustment--;
864        #      else if ( d->couples & Braces && ch == '}'
865        #                && ( at == d->coupleAttrib
866        #                     || (! at && hasRelevantOpening( KateDocCursor( line.line(), i, doc ) ))
867        #                   )
868        #              )
869        #        adjustment--;
870        #      else if ( d->couples & Brackets && ch == ']'
871        #                && ( at == d->coupleAttrib
872        #                     || (! at && hasRelevantOpening( KateDocCursor( line.line(), i, doc ) ))
873        #                   )
874        #              )
875        #        adjustment--;
876        #    }
877        #  }
878       
879        # Haven't figured out what that was for, so ignoring for now.
880       
881       
882        #  // check if we should indent, unless the line starts with comment text,
883        #  // or the match is in comment text
884        # Here we look at the line above the current line
885        if self.reIndentAfter:
886            match = self.reIndentAfter.search(above)
887            self.dprint(above)
888            if match:
889                self.dprint("reIndentAfter: found %s at %d" % (match.group(0), match.start(0)))
890                adjustment += 1
891
892        #  // else, check if this line should indent unless ...
893        #  ktl = doc->plainKateTextLine( line.line() );
894        #  if ( ! d->reIndent.isEmpty()
895        #         && (matchpos = d->reIndent.search( doc->textLine( line.line() ) )) > -1
896        #         && ! ISCOMMENT )
897        #    adjustment++;
898        fc = stc.GetLineIndentPosition(linenum)
899        lc = stc.GetLineEndPosition(linenum)
900        s = stc.GetStyleAt(fc)
901        if self.reIndent and lc > fc and not stc.isStyleComment(s):
902            text = stc.GetTextRange(fc, lc)
903            match = self.reIndent.search(text)
904            self.dprint(text)
905            if match:
906                self.dprint("reIndent: found %s at %d" % (match.group(0), match.start(0)))
907                adjustment += 1
908       
909        #  // else, check if the current line indicates if we should remove indentation unless ...
910        if self.reUnindent and lc > fc and not stc.isStyleComment(s):
911            text = stc.GetTextRange(fc, lc)
912            match = self.reUnindent.search(text)
913            if match:
914                self.dprint("reUnndent: found %s at %d" % (match.group(0), match.start(0)))
915                adjustment -= 1
916       
917        # Find the actual number of spaces to indent
918        if adjustment > 0:
919            pos += stc.GetIndent()
920        elif adjustment < 0:
921            pos -= stc.GetIndent()
922        if pos < 0:
923            pos = 0
924       
925        return pos
926   
927    def coupleBalance(self, stc, ln, open, close):
928        """Search the line to see if there are unmatched braces
929       
930        Search the line for unmatched braces given the open and close matching
931        pair.  This takes into account the style of the document to make sure
932        that the brace isn't in a comment or string.
933       
934        @param stc: the StyledTextCtrl instance
935        @param ln: line number
936        @param open: the opening brace character, e.g. "("
937        @param close: the complimentary closing brace character, e.g. ")"
938       
939        @return: brace mismatch count: 0 for matching braces, positive for a
940        surplus of opening braces, and negative for a surplus of closing braces
941        """
942        if ln < 0:
943            return 0
944        r = 0
945        fc = stc.GetLineIndentPosition(ln)
946        lc = stc.GetLineEndPosition(ln)
947        line = stc.GetStyledText(fc, lc)
948        self.dprint(repr(line))
949        i = len(line)
950        while i > 0:
951            i -= 1
952            s = line[i]
953            i -= 1
954            c = line[i]
955            if c == open and not (stc.isStyleComment(s) and stc.isStyleString(s)):
956                self.dprint("found %s at column %d" % (open, i/2))
957                r += 1
958            elif c == close and not (stc.isStyleComment(s) and stc.isStyleString(s)):
959                self.dprint("found %s at column %d" % (close, i/2))
960                r -= 1
961        return r
962           
963        #
964        #bool KateVarIndent::hasRelevantOpening( const KateDocCursor &end ) const
965        #{
966        #  KateDocCursor cur = end;
967        #  int count = 1;
968        #
969        #  QChar close = cur.currentChar();
970        #  QChar opener;
971        #  if ( close == '}' ) opener = '{';
972        #  else if ( close = ')' ) opener = '(';
973        #  else if (close = ']' ) opener = '[';
974        #  else return false;
975        #
976        #  //Move backwards 1 by 1 and find the opening partner
977        #  while (cur.moveBackward(1))
978        #  {
979        #    if (cur.currentAttrib() == d->coupleAttrib)
980        #    {
981        #      QChar ch = cur.currentChar();
982        #      if (ch == opener)
983        #        count--;
984        #      else if (ch == close)
985        #        count++;
986        #
987        #      if (count == 0)
988        #        return true;
989        #    }
990        #  }
991        #
992        #  return false;
993        #}
994        #
995        #
996        #//END KateVarIndent
997
998
999class NullAutoindent(debugmixin):
1000    """No-op unindenter that doesn't change the indent level at all.
1001    """
1002    def findIndent(self, stc, linenum):
1003        """No-op that returns the current indent level."""
1004        return stc.GetLineIndentation(linenum)
1005   
1006    def reindentLine(self, stc, linenum=None, dedent_only=False):
1007        """No-op that doesn't change the current indent level."""
1008        return stc.GetCurrentPos()
1009   
1010    def processReturn(self, stc):
1011        """Add a newline only."""
1012        linesep = stc.getLinesep()
1013        stc.AddText(linesep)
1014   
1015    def processTab(self, stc):
1016        """Don't reindent but insert the equivalent of a tab character"""
1017        stc.AddText(stc.GetIndentString(stc.GetIndent()))
1018   
1019    def electricChar(self, stc, uchar):
1020        """No electric chars in Null autoindenter."""
1021        return False
1022   
1023    def electricDelete(self, stc):
1024        """Defaults to standard delete processing in Null autoindenter."""
1025        stc.CmdKeyExecute(wx.stc.STC_CMD_CLEAR)
1026   
1027    def electricBackspace(self, stc):
1028        """Defaults to standard backspace processing in Null autoindenter."""
1029        stc.CmdKeyExecute(wx.stc.STC_CMD_DELETEBACK)
Note: See TracBrowser for help on using the browser.