|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a typing completer for Python. |
|
8 """ |
|
9 |
|
10 import re |
|
11 |
|
12 from PyQt5.Qsci import QsciLexerPython, QsciScintilla |
|
13 |
|
14 from .CompleterBase import CompleterBase |
|
15 |
|
16 import Preferences |
|
17 from Utilities import rxIndex |
|
18 |
|
19 |
|
20 class 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 ) |