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