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