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