eric7/QScintilla/TypingCompleters/CompleterPython.py

branch
eric7
changeset 8312
800c432b34c8
parent 8228
772103b14c18
child 8318
962bce857696
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/QScintilla/TypingCompleters/CompleterPython.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,496 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a typing completer for Python.
+"""
+
+import re
+
+from PyQt5.Qsci import QsciLexerPython, QsciScintilla
+
+from .CompleterBase import CompleterBase
+
+import Preferences
+from Utilities import rxIndex
+
+
+class CompleterPython(CompleterBase):
+    """
+    Class implementing typing completer for Python.
+    """
+    def __init__(self, editor, parent=None):
+        """
+        Constructor
+        
+        @param editor reference to the editor object (QScintilla.Editor)
+        @param parent reference to the parent object (QObject)
+        """
+        super().__init__(editor, parent)
+        
+        self.__defRX = re.compile(
+            r"^[ \t]*(def|cdef|cpdef) \w+\(")
+        self.__defSelfRX = re.compile(
+            r"^[ \t]*(def|cdef|cpdef) \w+\([ \t]*self[ \t]*[,)]")
+        self.__defClsRX = re.compile(
+            r"^[ \t]*(def|cdef|cpdef) \w+\([ \t]*cls[ \t]*[,)]")
+        self.__classRX = re.compile(
+            r"^[ \t]*(cdef[ \t]+)?class \w+\(")
+        self.__importRX = re.compile(r"^[ \t]*from [\w.]+ ")
+        self.__classmethodRX = re.compile(r"^[ \t]*@classmethod")
+        self.__staticmethodRX = re.compile(r"^[ \t]*@staticmethod")
+        
+        self.__defOnlyRX = re.compile(r"^[ \t]*def ")
+        
+        self.__ifRX = re.compile(r"^[ \t]*if ")
+        self.__elifRX = re.compile(r"^[ \t]*elif ")
+        self.__elseRX = re.compile(r"^[ \t]*else:")
+        
+        self.__tryRX = re.compile(r"^[ \t]*try:")
+        self.__finallyRX = re.compile(r"^[ \t]*finally:")
+        self.__exceptRX = re.compile(r"^[ \t]*except ")
+        self.__exceptcRX = re.compile(r"^[ \t]*except:")
+        
+        self.__whileRX = re.compile(r"^[ \t]*while ")
+        self.__forRX = re.compile(r"^[ \t]*for ")
+        
+        self.readSettings()
+    
+    def readSettings(self):
+        """
+        Public slot called to reread the configuration parameters.
+        """
+        self.setEnabled(
+            Preferences.getEditorTyping("Python/EnabledTypingAids"))
+        self.__insertClosingBrace = Preferences.getEditorTyping(
+            "Python/InsertClosingBrace")
+        self.__indentBrace = Preferences.getEditorTyping(
+            "Python/IndentBrace")
+        self.__skipBrace = Preferences.getEditorTyping(
+            "Python/SkipBrace")
+        self.__insertQuote = Preferences.getEditorTyping(
+            "Python/InsertQuote")
+        self.__dedentElse = Preferences.getEditorTyping(
+            "Python/DedentElse")
+        self.__dedentExcept = Preferences.getEditorTyping(
+            "Python/DedentExcept")
+        self.__insertImport = Preferences.getEditorTyping(
+            "Python/InsertImport")
+        self.__importBraceType = Preferences.getEditorTyping(
+            "Python/ImportBraceType")
+        self.__insertSelf = Preferences.getEditorTyping(
+            "Python/InsertSelf")
+        self.__insertBlank = Preferences.getEditorTyping(
+            "Python/InsertBlank")
+        self.__colonDetection = Preferences.getEditorTyping(
+            "Python/ColonDetection")
+        self.__dedentDef = Preferences.getEditorTyping(
+            "Python/DedentDef")
+
+    def charAdded(self, charNumber):
+        """
+        Public slot called to handle the user entering a character.
+        
+        @param charNumber value of the character entered (integer)
+        """
+        char = chr(charNumber)
+        if char not in ['(', ')', '{', '}', '[', ']', ' ', ',', "'", '"',
+                        '\n', ':']:
+            return  # take the short route
+        
+        line, col = self.editor.getCursorPosition()
+        
+        if (
+            self.__inComment(line, col) or
+            (char != '"' and self.__inDoubleQuotedString()) or
+            (char != '"' and self.__inTripleDoubleQuotedString()) or
+            (char != "'" and self.__inSingleQuotedString()) or
+            (char != "'" and self.__inTripleSingleQuotedString())
+        ):
+            return
+        
+        # open parenthesis
+        # insert closing parenthesis and self
+        if char == '(':
+            txt = self.editor.text(line)[:col]
+            self.editor.beginUndoAction()
+            if (
+                self.__insertSelf and
+                self.__defRX.fullmatch(txt) is not None
+            ):
+                if self.__isClassMethodDef():
+                    self.editor.insert('cls')
+                    self.editor.setCursorPosition(line, col + 3)
+                elif self.__isStaticMethodDef():
+                    # nothing to insert
+                    pass
+                elif self.__isClassMethod():
+                    self.editor.insert('self')
+                    self.editor.setCursorPosition(line, col + 4)
+            if self.__insertClosingBrace:
+                if (
+                    self.__defRX.fullmatch(txt) is not None or
+                    self.__classRX.fullmatch(txt) is not None
+                ):
+                    self.editor.insert('):')
+                else:
+                    self.editor.insert(')')
+            self.editor.endUndoAction()
+        
+        # closing parenthesis
+        # skip matching closing parenthesis
+        elif char in [')', '}', ']']:
+            txt = self.editor.text(line)
+            if col < len(txt) and char == txt[col] and self.__skipBrace:
+                self.editor.setSelection(line, col, line, col + 1)
+                self.editor.removeSelectedText()
+        
+        # space
+        # insert import, dedent to if for elif, dedent to try for except,
+        # dedent def
+        elif char == ' ':
+            txt = self.editor.text(line)[:col]
+            if self.__insertImport and self.__importRX.fullmatch(txt):
+                self.editor.beginUndoAction()
+                if self.__importBraceType:
+                    self.editor.insert('import ()')
+                    self.editor.setCursorPosition(line, col + 8)
+                else:
+                    self.editor.insert('import ')
+                    self.editor.setCursorPosition(line, col + 7)
+                self.editor.endUndoAction()
+            elif self.__dedentElse and self.__elifRX.fullmatch(txt):
+                self.__dedentToIf()
+            elif self.__dedentExcept and self.__exceptRX.fullmatch(txt):
+                self.__dedentExceptToTry(False)
+            elif self.__dedentDef and self.__defOnlyRX.fullmatch(txt):
+                self.__dedentDefStatement()
+        
+        # comma
+        # insert blank
+        elif char == ',' and self.__insertBlank:
+            self.editor.insert(' ')
+            self.editor.setCursorPosition(line, col + 1)
+        
+        # open curly brace
+        # insert closing brace
+        elif char == '{' and self.__insertClosingBrace:
+            self.editor.insert('}')
+        
+        # open bracket
+        # insert closing bracket
+        elif char == '[' and self.__insertClosingBrace:
+            self.editor.insert(']')
+        
+        # double quote
+        # insert double quote
+        elif char == '"' and self.__insertQuote:
+            self.editor.insert('"')
+        
+        # quote
+        # insert quote
+        elif char == '\'' and self.__insertQuote:
+            self.editor.insert('\'')
+        
+        # colon
+        # skip colon, dedent to if for else:
+        elif char == ':':
+            text = self.editor.text(line)
+            if col < len(text) and char == text[col]:
+                if self.__colonDetection:
+                    self.editor.setSelection(line, col, line, col + 1)
+                    self.editor.removeSelectedText()
+            else:
+                txt = text[:col]
+                if self.__dedentElse and self.__elseRX.fullmatch(txt):
+                    self.__dedentElseToIfWhileForTry()
+                elif self.__dedentExcept and self.__exceptcRX.fullmatch(txt):
+                    self.__dedentExceptToTry(True)
+                elif self.__dedentExcept and self.__finallyRX.fullmatch(txt):
+                    self.__dedentFinallyToTry()
+        
+        # new line
+        # indent to opening brace
+        elif char == '\n' and self.__indentBrace:
+            txt = self.editor.text(line - 1)
+            if re.search(":\r?\n", txt) is None:
+                self.editor.beginUndoAction()
+                stxt = txt.strip()
+                if stxt and stxt[-1] in ("(", "[", "{"):
+                    # indent one more level
+                    self.editor.indent(line)
+                    self.editor.editorCommand(QsciScintilla.SCI_VCHOME)
+                else:
+                    # indent to the level of the opening brace
+                    openCount = len(re.findall("[({[]", txt))
+                    closeCount = len(re.findall(r"[)}\]]", txt))
+                    if openCount > closeCount:
+                        openCount = 0
+                        closeCount = 0
+                        openList = list(re.finditer("[({[]", txt))
+                        index = len(openList) - 1
+                        while index > -1 and openCount == closeCount:
+                            lastOpenIndex = openList[index].start()
+                            txt2 = txt[lastOpenIndex:]
+                            openCount = len(re.findall("[({[]", txt2))
+                            closeCount = len(re.findall(r"[)}\]]", txt2))
+                            index -= 1
+                        if openCount > closeCount and lastOpenIndex > col:
+                            self.editor.insert(
+                                ' ' * (lastOpenIndex - col + 1))
+                            self.editor.setCursorPosition(
+                                line, lastOpenIndex + 1)
+                self.editor.endUndoAction()
+   
+    def __dedentToIf(self):
+        """
+        Private method to dedent the last line to the last if statement with
+        less (or equal) indentation.
+        """
+        line, col = self.editor.getCursorPosition()
+        indentation = self.editor.indentation(line)
+        ifLine = line - 1
+        while ifLine >= 0:
+            txt = self.editor.text(ifLine)
+            edInd = self.editor.indentation(ifLine)
+            if rxIndex(self.__elseRX, txt) == 0 and edInd <= indentation:
+                indentation = edInd - 1
+            elif (rxIndex(self.__ifRX, txt) == 0 or
+                  rxIndex(self.__elifRX, txt) == 0) and edInd <= indentation:
+                self.editor.cancelList()
+                self.editor.setIndentation(line, edInd)
+                break
+            ifLine -= 1
+    
+    def __dedentElseToIfWhileForTry(self):
+        """
+        Private method to dedent the line of the else statement to the last
+        if, while, for or try statement with less (or equal) indentation.
+        """
+        line, col = self.editor.getCursorPosition()
+        indentation = self.editor.indentation(line)
+        if line > 0:
+            prevInd = self.editor.indentation(line - 1)
+        ifLine = line - 1
+        while ifLine >= 0:
+            txt = self.editor.text(ifLine)
+            edInd = self.editor.indentation(ifLine)
+            if (
+                (rxIndex(self.__elseRX, txt) == 0 and
+                 edInd <= indentation) or
+                (rxIndex(self.__elifRX, txt) == 0 and
+                 edInd == indentation and
+                 edInd == prevInd)
+            ):
+                indentation = edInd - 1
+            elif (
+                (rxIndex(self.__ifRX, txt) == 0 or
+                 rxIndex(self.__whileRX, txt) == 0 or
+                 rxIndex(self.__forRX, txt) == 0 or
+                 rxIndex(self.__tryRX, txt) == 0) and
+                edInd <= indentation
+            ):
+                self.editor.cancelList()
+                self.editor.setIndentation(line, edInd)
+                break
+            ifLine -= 1
+    
+    def __dedentExceptToTry(self, hasColon):
+        """
+        Private method to dedent the line of the except statement to the last
+        try statement with less (or equal) indentation.
+        
+        @param hasColon flag indicating the except type (boolean)
+        """
+        line, col = self.editor.getCursorPosition()
+        indentation = self.editor.indentation(line)
+        tryLine = line - 1
+        while tryLine >= 0:
+            txt = self.editor.text(tryLine)
+            edInd = self.editor.indentation(tryLine)
+            if (
+                (rxIndex(self.__exceptcRX, txt) == 0 or
+                 rxIndex(self.__finallyRX, txt) == 0) and
+                edInd <= indentation
+            ):
+                indentation = edInd - 1
+            elif (rxIndex(self.__exceptRX, txt) == 0 or
+                  rxIndex(self.__tryRX, txt) == 0) and edInd <= indentation:
+                self.editor.cancelList()
+                self.editor.setIndentation(line, edInd)
+                break
+            tryLine -= 1
+    
+    def __dedentFinallyToTry(self):
+        """
+        Private method to dedent the line of the except statement to the last
+        try statement with less (or equal) indentation.
+        """
+        line, col = self.editor.getCursorPosition()
+        indentation = self.editor.indentation(line)
+        tryLine = line - 1
+        while tryLine >= 0:
+            txt = self.editor.text(tryLine)
+            edInd = self.editor.indentation(tryLine)
+            if rxIndex(self.__finallyRX, txt) == 0 and edInd <= indentation:
+                indentation = edInd - 1
+            elif (
+                (rxIndex(self.__tryRX, txt) == 0 or
+                 rxIndex(self.__exceptcRX, txt) == 0 or
+                 rxIndex(self.__exceptRX, txt) == 0) and
+                edInd <= indentation
+            ):
+                self.editor.cancelList()
+                self.editor.setIndentation(line, edInd)
+                break
+            tryLine -= 1
+    
+    def __dedentDefStatement(self):
+        """
+        Private method to dedent the line of the def statement to a previous
+        def statement or class statement.
+        """
+        line, col = self.editor.getCursorPosition()
+        indentation = self.editor.indentation(line)
+        tryLine = line - 1
+        while tryLine >= 0:
+            txt = self.editor.text(tryLine)
+            edInd = self.editor.indentation(tryLine)
+            newInd = -1
+            if rxIndex(self.__defRX, txt) == 0 and edInd < indentation:
+                newInd = edInd
+            elif rxIndex(self.__classRX, txt) == 0 and edInd < indentation:
+                newInd = edInd + (
+                    self.editor.indentationWidth() or self.editor.tabWidth()
+                )
+            if newInd >= 0:
+                self.editor.cancelList()
+                self.editor.setIndentation(line, newInd)
+                break
+            tryLine -= 1
+    
+    def __isClassMethod(self):
+        """
+        Private method to check, if the user is defining a class method.
+        
+        @return flag indicating the definition of a class method (boolean)
+        """
+        line, col = self.editor.getCursorPosition()
+        indentation = self.editor.indentation(line)
+        curLine = line - 1
+        while curLine >= 0:
+            txt = self.editor.text(curLine)
+            if (
+                ((rxIndex(self.__defSelfRX, txt) == 0 or
+                  rxIndex(self.__defClsRX, txt) == 0) and
+                 self.editor.indentation(curLine) == indentation) or
+                (rxIndex(self.__classRX, txt) == 0 and
+                 self.editor.indentation(curLine) < indentation)
+            ):
+                return True
+            elif (
+                rxIndex(self.__defRX, txt) == 0 and
+                self.editor.indentation(curLine) <= indentation
+            ):
+                return False
+            curLine -= 1
+        return False
+    
+    def __isClassMethodDef(self):
+        """
+        Private method to check, if the user is defing a class method
+        (@classmethod).
+        
+        @return flag indicating the definition of a class method (boolean)
+        """
+        line, col = self.editor.getCursorPosition()
+        indentation = self.editor.indentation(line)
+        curLine = line - 1
+        if (
+            rxIndex(self.__classmethodRX, self.editor.text(curLine)) == 0 and
+            self.editor.indentation(curLine) == indentation
+        ):
+            return True
+        return False
+    
+    def __isStaticMethodDef(self):
+        """
+        Private method to check, if the user is defing a static method
+        (@staticmethod) method.
+        
+        @return flag indicating the definition of a static method (boolean)
+        """
+        line, col = self.editor.getCursorPosition()
+        indentation = self.editor.indentation(line)
+        curLine = line - 1
+        if (
+            rxIndex(self.__staticmethodRX, self.editor.text(curLine)) == 0 and
+            self.editor.indentation(curLine) == indentation
+        ):
+            return True
+        return False
+    
+    def __inComment(self, line, col):
+        """
+        Private method to check, if the cursor is inside a comment.
+        
+        @param line current line (integer)
+        @param col current position within line (integer)
+        @return flag indicating, if the cursor is inside a comment (boolean)
+        """
+        txt = self.editor.text(line)
+        if col == len(txt):
+            col -= 1
+        while col >= 0:
+            if txt[col] == "#":
+                return True
+            col -= 1
+        return False
+    
+    def __inDoubleQuotedString(self):
+        """
+        Private method to check, if the cursor is within a double quoted
+        string.
+        
+        @return flag indicating, if the cursor is inside a double
+            quoted string (boolean)
+        """
+        return self.editor.currentStyle() == QsciLexerPython.DoubleQuotedString
+    
+    def __inTripleDoubleQuotedString(self):
+        """
+        Private method to check, if the cursor is within a triple double
+        quoted string.
+        
+        @return flag indicating, if the cursor is inside a triple double
+            quoted string (boolean)
+        """
+        return (
+            self.editor.currentStyle() ==
+            QsciLexerPython.TripleDoubleQuotedString
+        )
+    
+    def __inSingleQuotedString(self):
+        """
+        Private method to check, if the cursor is within a single quoted
+        string.
+        
+        @return flag indicating, if the cursor is inside a single
+            quoted string (boolean)
+        """
+        return self.editor.currentStyle() == QsciLexerPython.SingleQuotedString
+    
+    def __inTripleSingleQuotedString(self):
+        """
+        Private method to check, if the cursor is within a triple single
+        quoted string.
+        
+        @return flag indicating, if the cursor is inside a triple single
+            quoted string (boolean)
+        """
+        return (
+            self.editor.currentStyle() ==
+            QsciLexerPython.TripleSingleQuotedString
+        )

eric ide

mercurial