Wed, 30 Dec 2020 11:00:05 +0100
Updated copyright for 2021.
# -*- 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(CompleterPython, self).__init__(editor, parent) self.__defRX = re.compile(r"^[ \t]*def \w+\(") self.__defSelfRX = re.compile(r"^[ \t]*def \w+\([ \t]*self[ \t]*[,)]") self.__defClsRX = re.compile(r"^[ \t]*def \w+\([ \t]*cls[ \t]*[,)]") self.__classRX = re.compile(r"^[ \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]: if 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 == ',': if self.__insertBlank: self.editor.insert(' ') self.editor.setCursorPosition(line, col + 1) # open curly brace # insert closing brace elif char == '{': if self.__insertClosingBrace: self.editor.insert('}') # open bracket # insert closing bracket elif char == '[': if self.__insertClosingBrace: self.editor.insert(']') # double quote # insert double quote elif char == '"': if self.__insertQuote: self.editor.insert('"') # quote # insert quote elif char == '\'': if 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': if 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: indentation = edInd - 1 elif ( 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 ): return True elif ( 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 )