1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing a typing completer for Python. 8""" 9 10import re 11 12from PyQt5.Qsci import QsciLexerPython, QsciScintilla 13 14from .CompleterBase import CompleterBase 15 16import Preferences 17from Utilities import rxIndex 18 19 20class CompleterPython(CompleterBase): 21 """ 22 Class implementing typing completer for Python. 23 """ 24 def __init__(self, editor, parent=None): 25 """ 26 Constructor 27 28 @param editor reference to the editor object (QScintilla.Editor) 29 @param parent reference to the parent object (QObject) 30 """ 31 super().__init__(editor, parent) 32 33 self.__defRX = re.compile( 34 r"^[ \t]*(def|cdef|cpdef) \w+\(") 35 self.__defSelfRX = re.compile( 36 r"^[ \t]*(def|cdef|cpdef) \w+\([ \t]*self[ \t]*[,)]") 37 self.__defClsRX = re.compile( 38 r"^[ \t]*(def|cdef|cpdef) \w+\([ \t]*cls[ \t]*[,)]") 39 self.__classRX = re.compile( 40 r"^[ \t]*(cdef[ \t]+)?class \w+\(") 41 self.__importRX = re.compile(r"^[ \t]*from [\w.]+ ") 42 self.__classmethodRX = re.compile(r"^[ \t]*@classmethod") 43 self.__staticmethodRX = re.compile(r"^[ \t]*@staticmethod") 44 45 self.__defOnlyRX = re.compile(r"^[ \t]*def ") 46 47 self.__ifRX = re.compile(r"^[ \t]*if ") 48 self.__elifRX = re.compile(r"^[ \t]*elif ") 49 self.__elseRX = re.compile(r"^[ \t]*else:") 50 51 self.__tryRX = re.compile(r"^[ \t]*try:") 52 self.__finallyRX = re.compile(r"^[ \t]*finally:") 53 self.__exceptRX = re.compile(r"^[ \t]*except ") 54 self.__exceptcRX = re.compile(r"^[ \t]*except:") 55 56 self.__whileRX = re.compile(r"^[ \t]*while ") 57 self.__forRX = re.compile(r"^[ \t]*for ") 58 59 self.readSettings() 60 61 def readSettings(self): 62 """ 63 Public slot called to reread the configuration parameters. 64 """ 65 self.setEnabled( 66 Preferences.getEditorTyping("Python/EnabledTypingAids")) 67 self.__insertClosingBrace = Preferences.getEditorTyping( 68 "Python/InsertClosingBrace") 69 self.__indentBrace = Preferences.getEditorTyping( 70 "Python/IndentBrace") 71 self.__skipBrace = Preferences.getEditorTyping( 72 "Python/SkipBrace") 73 self.__insertQuote = Preferences.getEditorTyping( 74 "Python/InsertQuote") 75 self.__dedentElse = Preferences.getEditorTyping( 76 "Python/DedentElse") 77 self.__dedentExcept = Preferences.getEditorTyping( 78 "Python/DedentExcept") 79 self.__insertImport = Preferences.getEditorTyping( 80 "Python/InsertImport") 81 self.__importBraceType = Preferences.getEditorTyping( 82 "Python/ImportBraceType") 83 self.__insertSelf = Preferences.getEditorTyping( 84 "Python/InsertSelf") 85 self.__insertBlank = Preferences.getEditorTyping( 86 "Python/InsertBlank") 87 self.__colonDetection = Preferences.getEditorTyping( 88 "Python/ColonDetection") 89 self.__dedentDef = Preferences.getEditorTyping( 90 "Python/DedentDef") 91 92 def charAdded(self, charNumber): 93 """ 94 Public slot called to handle the user entering a character. 95 96 @param charNumber value of the character entered (integer) 97 """ 98 char = chr(charNumber) 99 if char not in ['(', ')', '{', '}', '[', ']', ' ', ',', "'", '"', 100 '\n', ':']: 101 return # take the short route 102 103 line, col = self.editor.getCursorPosition() 104 105 if ( 106 self.__inComment(line, col) or 107 (char != '"' and self.__inDoubleQuotedString()) or 108 (char != '"' and self.__inTripleDoubleQuotedString()) or 109 (char != "'" and self.__inSingleQuotedString()) or 110 (char != "'" and self.__inTripleSingleQuotedString()) 111 ): 112 return 113 114 # open parenthesis 115 # insert closing parenthesis and self 116 if char == '(': 117 txt = self.editor.text(line)[:col] 118 self.editor.beginUndoAction() 119 if ( 120 self.__insertSelf and 121 self.__defRX.fullmatch(txt) is not None 122 ): 123 if self.__isClassMethodDef(): 124 self.editor.insert('cls') 125 self.editor.setCursorPosition(line, col + 3) 126 elif self.__isStaticMethodDef(): 127 # nothing to insert 128 pass 129 elif self.__isClassMethod(): 130 self.editor.insert('self') 131 self.editor.setCursorPosition(line, col + 4) 132 if self.__insertClosingBrace: 133 if ( 134 self.__defRX.fullmatch(txt) is not None or 135 self.__classRX.fullmatch(txt) is not None 136 ): 137 self.editor.insert('):') 138 else: 139 self.editor.insert(')') 140 self.editor.endUndoAction() 141 142 # closing parenthesis 143 # skip matching closing parenthesis 144 elif char in [')', '}', ']']: 145 txt = self.editor.text(line) 146 if col < len(txt) and char == txt[col] and self.__skipBrace: 147 self.editor.setSelection(line, col, line, col + 1) 148 self.editor.removeSelectedText() 149 150 # space 151 # insert import, dedent to if for elif, dedent to try for except, 152 # dedent def 153 elif char == ' ': 154 txt = self.editor.text(line)[:col] 155 if self.__insertImport and self.__importRX.fullmatch(txt): 156 self.editor.beginUndoAction() 157 if self.__importBraceType: 158 self.editor.insert('import ()') 159 self.editor.setCursorPosition(line, col + 8) 160 else: 161 self.editor.insert('import ') 162 self.editor.setCursorPosition(line, col + 7) 163 self.editor.endUndoAction() 164 elif self.__dedentElse and self.__elifRX.fullmatch(txt): 165 self.__dedentToIf() 166 elif self.__dedentExcept and self.__exceptRX.fullmatch(txt): 167 self.__dedentExceptToTry(False) 168 elif self.__dedentDef and self.__defOnlyRX.fullmatch(txt): 169 self.__dedentDefStatement() 170 171 # comma 172 # insert blank 173 elif char == ',' and self.__insertBlank: 174 self.editor.insert(' ') 175 self.editor.setCursorPosition(line, col + 1) 176 177 # open curly brace 178 # insert closing brace 179 elif char == '{' and self.__insertClosingBrace: 180 self.editor.insert('}') 181 182 # open bracket 183 # insert closing bracket 184 elif char == '[' and self.__insertClosingBrace: 185 self.editor.insert(']') 186 187 # double quote 188 # insert double quote 189 elif char == '"' and self.__insertQuote: 190 self.editor.insert('"') 191 192 # quote 193 # insert quote 194 elif char == '\'' and self.__insertQuote: 195 self.editor.insert('\'') 196 197 # colon 198 # skip colon, dedent to if for else: 199 elif char == ':': 200 text = self.editor.text(line) 201 if col < len(text) and char == text[col]: 202 if self.__colonDetection: 203 self.editor.setSelection(line, col, line, col + 1) 204 self.editor.removeSelectedText() 205 else: 206 txt = text[:col] 207 if self.__dedentElse and self.__elseRX.fullmatch(txt): 208 self.__dedentElseToIfWhileForTry() 209 elif self.__dedentExcept and self.__exceptcRX.fullmatch(txt): 210 self.__dedentExceptToTry(True) 211 elif self.__dedentExcept and self.__finallyRX.fullmatch(txt): 212 self.__dedentFinallyToTry() 213 214 # new line 215 # indent to opening brace 216 elif char == '\n' and self.__indentBrace: 217 txt = self.editor.text(line - 1) 218 if re.search(":\r?\n", txt) is None: 219 self.editor.beginUndoAction() 220 stxt = txt.strip() 221 if stxt and stxt[-1] in ("(", "[", "{"): 222 # indent one more level 223 self.editor.indent(line) 224 self.editor.editorCommand(QsciScintilla.SCI_VCHOME) 225 else: 226 # indent to the level of the opening brace 227 openCount = len(re.findall("[({[]", txt)) 228 closeCount = len(re.findall(r"[)}\]]", txt)) 229 if openCount > closeCount: 230 openCount = 0 231 closeCount = 0 232 openList = list(re.finditer("[({[]", txt)) 233 index = len(openList) - 1 234 while index > -1 and openCount == closeCount: 235 lastOpenIndex = openList[index].start() 236 txt2 = txt[lastOpenIndex:] 237 openCount = len(re.findall("[({[]", txt2)) 238 closeCount = len(re.findall(r"[)}\]]", txt2)) 239 index -= 1 240 if openCount > closeCount and lastOpenIndex > col: 241 self.editor.insert( 242 ' ' * (lastOpenIndex - col + 1)) 243 self.editor.setCursorPosition( 244 line, lastOpenIndex + 1) 245 self.editor.endUndoAction() 246 247 def __dedentToIf(self): 248 """ 249 Private method to dedent the last line to the last if statement with 250 less (or equal) indentation. 251 """ 252 line, col = self.editor.getCursorPosition() 253 indentation = self.editor.indentation(line) 254 ifLine = line - 1 255 while ifLine >= 0: 256 txt = self.editor.text(ifLine) 257 edInd = self.editor.indentation(ifLine) 258 if rxIndex(self.__elseRX, txt) == 0 and edInd <= indentation: 259 indentation = edInd - 1 260 elif (rxIndex(self.__ifRX, txt) == 0 or 261 rxIndex(self.__elifRX, txt) == 0) and edInd <= indentation: 262 self.editor.cancelList() 263 self.editor.setIndentation(line, edInd) 264 break 265 ifLine -= 1 266 267 def __dedentElseToIfWhileForTry(self): 268 """ 269 Private method to dedent the line of the else statement to the last 270 if, while, for or try statement with less (or equal) indentation. 271 """ 272 line, col = self.editor.getCursorPosition() 273 indentation = self.editor.indentation(line) 274 if line > 0: 275 prevInd = self.editor.indentation(line - 1) 276 ifLine = line - 1 277 while ifLine >= 0: 278 txt = self.editor.text(ifLine) 279 edInd = self.editor.indentation(ifLine) 280 if ( 281 (rxIndex(self.__elseRX, txt) == 0 and 282 edInd <= indentation) or 283 (rxIndex(self.__elifRX, txt) == 0 and 284 edInd == indentation and 285 edInd == prevInd) 286 ): 287 indentation = edInd - 1 288 elif ( 289 (rxIndex(self.__ifRX, txt) == 0 or 290 rxIndex(self.__whileRX, txt) == 0 or 291 rxIndex(self.__forRX, txt) == 0 or 292 rxIndex(self.__tryRX, txt) == 0) and 293 edInd <= indentation 294 ): 295 self.editor.cancelList() 296 self.editor.setIndentation(line, edInd) 297 break 298 ifLine -= 1 299 300 def __dedentExceptToTry(self, hasColon): 301 """ 302 Private method to dedent the line of the except statement to the last 303 try statement with less (or equal) indentation. 304 305 @param hasColon flag indicating the except type (boolean) 306 """ 307 line, col = self.editor.getCursorPosition() 308 indentation = self.editor.indentation(line) 309 tryLine = line - 1 310 while tryLine >= 0: 311 txt = self.editor.text(tryLine) 312 edInd = self.editor.indentation(tryLine) 313 if ( 314 (rxIndex(self.__exceptcRX, txt) == 0 or 315 rxIndex(self.__finallyRX, txt) == 0) and 316 edInd <= indentation 317 ): 318 indentation = edInd - 1 319 elif (rxIndex(self.__exceptRX, txt) == 0 or 320 rxIndex(self.__tryRX, txt) == 0) and edInd <= indentation: 321 self.editor.cancelList() 322 self.editor.setIndentation(line, edInd) 323 break 324 tryLine -= 1 325 326 def __dedentFinallyToTry(self): 327 """ 328 Private method to dedent the line of the except statement to the last 329 try statement with less (or equal) indentation. 330 """ 331 line, col = self.editor.getCursorPosition() 332 indentation = self.editor.indentation(line) 333 tryLine = line - 1 334 while tryLine >= 0: 335 txt = self.editor.text(tryLine) 336 edInd = self.editor.indentation(tryLine) 337 if rxIndex(self.__finallyRX, txt) == 0 and edInd <= indentation: 338 indentation = edInd - 1 339 elif ( 340 (rxIndex(self.__tryRX, txt) == 0 or 341 rxIndex(self.__exceptcRX, txt) == 0 or 342 rxIndex(self.__exceptRX, txt) == 0) and 343 edInd <= indentation 344 ): 345 self.editor.cancelList() 346 self.editor.setIndentation(line, edInd) 347 break 348 tryLine -= 1 349 350 def __dedentDefStatement(self): 351 """ 352 Private method to dedent the line of the def statement to a previous 353 def statement or class statement. 354 """ 355 line, col = self.editor.getCursorPosition() 356 indentation = self.editor.indentation(line) 357 tryLine = line - 1 358 while tryLine >= 0: 359 txt = self.editor.text(tryLine) 360 edInd = self.editor.indentation(tryLine) 361 newInd = -1 362 if rxIndex(self.__defRX, txt) == 0 and edInd < indentation: 363 newInd = edInd 364 elif rxIndex(self.__classRX, txt) == 0 and edInd < indentation: 365 newInd = edInd + ( 366 self.editor.indentationWidth() or self.editor.tabWidth() 367 ) 368 if newInd >= 0: 369 self.editor.cancelList() 370 self.editor.setIndentation(line, newInd) 371 break 372 tryLine -= 1 373 374 def __isClassMethod(self): 375 """ 376 Private method to check, if the user is defining a class method. 377 378 @return flag indicating the definition of a class method (boolean) 379 """ 380 line, col = self.editor.getCursorPosition() 381 indentation = self.editor.indentation(line) 382 curLine = line - 1 383 while curLine >= 0: 384 txt = self.editor.text(curLine) 385 if ( 386 ((rxIndex(self.__defSelfRX, txt) == 0 or 387 rxIndex(self.__defClsRX, txt) == 0) and 388 self.editor.indentation(curLine) == indentation) or 389 (rxIndex(self.__classRX, txt) == 0 and 390 self.editor.indentation(curLine) < indentation) 391 ): 392 return True 393 elif ( 394 rxIndex(self.__defRX, txt) == 0 and 395 self.editor.indentation(curLine) <= indentation 396 ): 397 return False 398 curLine -= 1 399 return False 400 401 def __isClassMethodDef(self): 402 """ 403 Private method to check, if the user is defing a class method 404 (@classmethod). 405 406 @return flag indicating the definition of a class method (boolean) 407 """ 408 line, col = self.editor.getCursorPosition() 409 indentation = self.editor.indentation(line) 410 curLine = line - 1 411 if ( 412 rxIndex(self.__classmethodRX, self.editor.text(curLine)) == 0 and 413 self.editor.indentation(curLine) == indentation 414 ): 415 return True 416 return False 417 418 def __isStaticMethodDef(self): 419 """ 420 Private method to check, if the user is defing a static method 421 (@staticmethod) method. 422 423 @return flag indicating the definition of a static method (boolean) 424 """ 425 line, col = self.editor.getCursorPosition() 426 indentation = self.editor.indentation(line) 427 curLine = line - 1 428 if ( 429 rxIndex(self.__staticmethodRX, self.editor.text(curLine)) == 0 and 430 self.editor.indentation(curLine) == indentation 431 ): 432 return True 433 return False 434 435 def __inComment(self, line, col): 436 """ 437 Private method to check, if the cursor is inside a comment. 438 439 @param line current line (integer) 440 @param col current position within line (integer) 441 @return flag indicating, if the cursor is inside a comment (boolean) 442 """ 443 txt = self.editor.text(line) 444 if col == len(txt): 445 col -= 1 446 while col >= 0: 447 if txt[col] == "#": 448 return True 449 col -= 1 450 return False 451 452 def __inDoubleQuotedString(self): 453 """ 454 Private method to check, if the cursor is within a double quoted 455 string. 456 457 @return flag indicating, if the cursor is inside a double 458 quoted string (boolean) 459 """ 460 return self.editor.currentStyle() == QsciLexerPython.DoubleQuotedString 461 462 def __inTripleDoubleQuotedString(self): 463 """ 464 Private method to check, if the cursor is within a triple double 465 quoted string. 466 467 @return flag indicating, if the cursor is inside a triple double 468 quoted string (boolean) 469 """ 470 return ( 471 self.editor.currentStyle() == 472 QsciLexerPython.TripleDoubleQuotedString 473 ) 474 475 def __inSingleQuotedString(self): 476 """ 477 Private method to check, if the cursor is within a single quoted 478 string. 479 480 @return flag indicating, if the cursor is inside a single 481 quoted string (boolean) 482 """ 483 return self.editor.currentStyle() == QsciLexerPython.SingleQuotedString 484 485 def __inTripleSingleQuotedString(self): 486 """ 487 Private method to check, if the cursor is within a triple single 488 quoted string. 489 490 @return flag indicating, if the cursor is inside a triple single 491 quoted string (boolean) 492 """ 493 return ( 494 self.editor.currentStyle() == 495 QsciLexerPython.TripleSingleQuotedString 496 ) 497