QScintilla/SpellChecker.py

changeset 0
de9c2efb9d02
child 6
52e8c820d0dd
equal deleted inserted replaced
-1:000000000000 0:de9c2efb9d02
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2008 - 2009 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the spell checker for the editor component.
8
9 The spell checker is based on pyenchant.
10 """
11
12 try:
13 import enchant
14 except (ImportError, AttributeError, OSError):
15 pass
16
17 import os
18
19 from PyQt4.QtCore import *
20
21 import Preferences
22 import Utilities
23
24 class SpellChecker(QObject):
25 """
26 Class implementing a pyenchant based spell checker.
27 """
28 # class attributes to be used as defaults
29 _spelling_lang = None
30 _spelling_dict = None
31
32 def __init__(self, editor, indicator, defaultLanguage = None, checkRegion = None):
33 """
34 Constructor
35
36 @param editor reference to the editor object (QScintilla.Editor)
37 @param indicator spell checking indicator
38 @keyparam defaultLanguage the language to be used as the default (string).
39 The string should be in language locale format (e.g. en_US, de).
40 @keyparam checkRegion reference to a function to check for a valid region
41 """
42 self.editor = editor
43 self.indicator = indicator
44 if defaultLanguage is not None:
45 self.setDefaultLanguage(defaultLanguage)
46 if checkRegion is not None:
47 self.__checkRegion = checkRegion
48 else:
49 self.__checkRegion = lambda r: True
50 self.minimumWordSize = 3
51 self.lastCheckedLine = -1
52
53 self.__ignoreWords = []
54 self.__replaceWords = {}
55
56 @classmethod
57 def getAvailableLanguages(cls):
58 """
59 Public classmethod to get all available languages.
60
61 @return list of available languages (list of strings)
62 """
63 try:
64 return enchant.list_languages()
65 except NameError:
66 pass
67 return []
68
69 @classmethod
70 def isAvailable(cls):
71 """
72 Public classmethod to check, if spellchecking is available.
73
74 @return flag indicating availability (boolean)
75 """
76 if Preferences.getEditor("SpellCheckingEnabled"):
77 try:
78 return len(enchant.list_languages()) > 0
79 except NameError:
80 pass
81 return False
82
83 @classmethod
84 def _getDict(cls, lang, pwl = "", pel = ""):
85 """
86 Protected classmethod to get a new dictionary.
87
88 @param lang the language to be used as the default (string).
89 The string should be in language locale format (e.g. en_US, de).
90 @keyparam pwl name of the personal/project word list (string)
91 @keyparam pel name of the personal/project exclude list (string)
92 @return reference to the dictionary (enchant.Dict)
93 """
94 if not pwl:
95 pwl = unicode(Preferences.getEditor("SpellCheckingPersonalWordList"))
96 if not pwl:
97 pwl = os.path.join(Utilities.getConfigDir(), "spelling", "pwl.dic")
98 d = os.path.dirname(pwl)
99 if not os.path.exists(d):
100 os.makedirs(d)
101
102 if not pel:
103 pel = unicode(Preferences.getEditor("SpellCheckingPersonalExcludeList"))
104 if not pel:
105 pel = os.path.join(Utilities.getConfigDir(), "spelling", "pel.dic")
106 d = os.path.dirname(pel)
107 if not os.path.exists(d):
108 os.makedirs(d)
109
110 try:
111 d = enchant.DictWithPWL(lang, pwl, pel)
112 except:
113 # Catch all exceptions, because if pyenchant isn't available, you
114 # can't catch the enchant.DictNotFound error.
115 d = None
116 return d
117
118 @classmethod
119 def setDefaultLanguage(cls, language):
120 """
121 Public classmethod to set the default language.
122
123 @param language the language to be used as the default (string).
124 The string should be in language locale format (e.g. en_US, de).
125 """
126 cls._spelling_lang = language
127 cls._spelling_dict = cls._getDict(language)
128
129 def setLanguage(self, language, pwl = "", pel = ""):
130 """
131 Public method to set the current language.
132
133 @param language the language to be used as the default (string).
134 The string should be in language locale format (e.g. en_US, de).
135 @keyparam pwl name of the personal/project word list (string)
136 @keyparam pel name of the personal/project exclude list (string)
137 """
138 self._spelling_lang = language
139 self._spelling_dict = self._getDict(language, pwl = pwl,
140 pel = pel)
141
142 def getLanguage(self):
143 """
144 Public method to get the current language.
145
146 @return current language in language locale format (string)
147 """
148 return self._spelling_lang
149
150 def setMinimumWordSize(self, size):
151 """
152 Public method to set the minimum word size.
153
154 @param size minimum word size (integer)
155 """
156 self.minimumWordSize = size
157
158 def __getNextWord(self, pos, endPosition):
159 """
160 Private method to get the next word in the text after the given position.
161
162 @param pos position to start word extraction (integer)
163 @param endPosition position to stop word extraction (integer)
164 @return tuple of three values (the extracted word (string),
165 start position (integer), end position (integer))
166 """
167 if pos < 0 or pos >= endPosition:
168 return "", -1, -1
169
170 ch = self.editor.charAt(pos)
171 # 1. skip non-word characters
172 while pos < endPosition and not ch.isalnum():
173 pos = self.editor.positionAfter(pos)
174 ch = self.editor.charAt(pos)
175 if pos == endPosition:
176 return "", -1, -1
177 startPos = pos
178
179 # 2. extract the word
180 word = ""
181 while pos < endPosition and ch.isalnum():
182 word += ch
183 pos = self.editor.positionAfter(pos)
184 ch = self.editor.charAt(pos)
185 endPos = pos
186 if word.isdigit():
187 return self.__getNextWord(endPos, endPosition)
188 else:
189 return word, startPos, endPos
190
191 def getContext(self, wordStart, wordEnd):
192 """
193 Public method to get the context of a faulty word.
194
195 @param wordStart the starting position of the word (integer)
196 @param wordEnd the ending position of the word (integer)
197 @return tuple of the leading and trailing context (string, string)
198 """
199 sline, sindex = self.editor.lineIndexFromPosition(wordStart)
200 eline, eindex = self.editor.lineIndexFromPosition(wordEnd)
201 text = self.editor.text(sline)
202 return (text[:sindex], text[eindex:])
203
204 def getError(self):
205 """
206 Public method to get information about the last error found.
207
208 @return tuple of last faulty word (string), starting position of the
209 faulty word (integer) and ending position of the faulty word (integer)
210 """
211 return (self.word, self.wordStart, self.wordEnd)
212
213 def initCheck(self, startPos, endPos):
214 """
215 Public method to initialize a spell check.
216
217 @param startPos position to start at (integer)
218 @param endPos position to end at (integer)
219 @return flag indicating successful initialization (boolean)
220 """
221 if startPos == endPos:
222 return False
223
224 spell = self._spelling_dict
225 if spell is None:
226 return False
227
228 self.editor.clearIndicatorRange(self.indicator, startPos, endPos - startPos)
229
230 self.pos = startPos
231 self.endPos = endPos
232 self.word = ""
233 self.wordStart = -1
234 self.wordEnd = -1
235 return True
236
237 def __checkDocumentPart(self, startPos, endPos):
238 """
239 Private method to check some part of the document.
240
241 @param startPos position to start at (integer)
242 @param endPos position to end at (integer)
243 """
244 if not self.initCheck(startPos, endPos):
245 return
246
247 while True:
248 try:
249 self.next()
250 self.editor.setIndicatorRange(self.indicator, self.wordStart,
251 self.wordEnd - self.wordStart)
252 except StopIteration:
253 break
254
255 def __incrementalCheck(self):
256 """
257 Private method to check the document incrementally.
258 """
259 if self.lastCheckedLine < 0:
260 return
261
262 linesChunk = Preferences.getEditor("AutoSpellCheckChunkSize")
263 self.checkLines(self.lastCheckedLine, self.lastCheckedLine + linesChunk)
264 self.lastCheckedLine = self.lastCheckedLine + linesChunk + 1
265 if self.lastCheckedLine >= self.editor.lines():
266 self.lastCheckedLine = -1
267 else:
268 QTimer.singleShot(0, self.__incrementalCheck)
269
270 def checkWord(self, pos, atEnd = False):
271 """
272 Public method to check the word at position pos.
273
274 @param pos position to check at (integer)
275 @keyparam atEnd flag indicating the position is at the end of the word
276 to check (boolean)
277 """
278 spell = self._spelling_dict
279 if spell is None:
280 return
281
282 if atEnd:
283 pos = self.editor.positionBefore(pos)
284
285 if pos >= 0 and self.__checkRegion(pos):
286 if not self.editor.charAt(pos).isalnum():
287 pos = self.editor.positionBefore(pos)
288
289 if self.editor.charAt(pos).isalnum():
290 line, index = self.editor.lineIndexFromPosition(pos)
291 word = self.editor.getWord(line, index, useWordChars = False)
292 if len(word) >= self.minimumWordSize:
293 ok = spell.check(word)
294 start, end = \
295 self.editor.getWordBoundaries(line, index, useWordChars = False)
296 if ok:
297 self.editor.clearIndicator(self.indicator, line, start, line, end)
298 else:
299 # spell check indicated an error
300 self.editor.setIndicator(self.indicator, line, start, line, end)
301
302 def checkLines(self, firstLine, lastLine):
303 """
304 Public method to check some lines of text.
305
306 @param firstLine line number of first line to check (integer)
307 @param lastLine line number of last line to check (integer)
308 """
309 startPos = self.editor.positionFromLineIndex(firstLine, 0)
310
311 if lastLine >= self.editor.lines():
312 lastLine = self.editor.lines() - 1
313 endPos = self.editor.lineEndPosition(lastLine)
314
315 self.__checkDocumentPart(startPos, endPos)
316
317 def checkDocument(self):
318 """
319 Public method to check the complete document
320 """
321 self.__checkDocumentPart(0, self.editor.length())
322
323 def checkDocumentIncrementally(self):
324 """
325 Public method to check the document incrementally.
326 """
327 spell = self._spelling_dict
328 if spell is None:
329 return
330
331 if Preferences.getEditor("AutoSpellCheckingEnabled"):
332 self.lastCheckedLine = 0
333 QTimer.singleShot(0, self.__incrementalCheck)
334
335 def stopIncrementalCheck(self):
336 """
337 Public method to stop an incremental check.
338 """
339 self.lastCheckedLine = -1
340
341 def checkSelection(self):
342 """
343 Private method to check the current selection.
344 """
345 selStartLine, selStartIndex, selEndLine, selEndIndex = \
346 self.editor.getSelection()
347 self.__checkDocumentPart(
348 self.editor.positionFromLineIndex(selStartLine, selStartIndex),
349 self.editor.positionFromLineIndex(selEndLine, selEndIndex)
350 )
351
352 def checkCurrentPage(self):
353 """
354 Private method to check the currently visible page.
355 """
356 startLine = self.editor.firstVisibleLine()
357 endLine = startLine + self.editor.linesOnScreen()
358 self.checkLines(startLine, endLine)
359
360 def clearAll(self):
361 """
362 Public method to clear all spelling markers.
363 """
364 self.editor.clearIndicatorRange(self.indicator, 0, self.editor.length())
365
366 def getSuggestions(self, word):
367 """
368 Public method to get suggestions for the given word.
369
370 @param word word to get suggestions for (string)
371 @return list of suggestions (list of strings)
372 """
373 spell = self._spelling_dict
374 if spell and len(word) >= self.minimumWordSize:
375 suggestions = spell.suggest(word)
376 return suggestions
377
378 return []
379
380 def add(self, word = None):
381 """
382 Public method to add a word to the personal word list.
383
384 @param word word to add (string)
385 """
386 spell = self._spelling_dict
387 if spell:
388 if word is None:
389 word = self.word
390 spell.add(word)
391
392 def remove(self, word):
393 """
394 Public method to add a word to the personal exclude list.
395
396 @param word word to add (string)
397 """
398 spell = self._spelling_dict
399 if spell:
400 spell.remove(word)
401
402 def ignoreAlways(self, word = None):
403 """
404 Public method to tell the checker, to always ignore the given word
405 or the current word.
406
407 @param word word to be ignored (string)
408 """
409 if word is None:
410 word = self.word
411 if word not in self.__ignoreWords:
412 self.__ignoreWords.append(word)
413
414 def replace(self, replacement):
415 """
416 Public method to tell the checker to replace the current word with
417 the replacement string.
418
419 @param replacement replacement string (string)
420 """
421 sline, sindex = self.editor.lineIndexFromPosition(self.wordStart)
422 eline, eindex = self.editor.lineIndexFromPosition(self.wordEnd)
423 self.editor.setSelection(sline, sindex, eline, eindex)
424 self.editor.beginUndoAction()
425 self.editor.removeSelectedText()
426 self.editor.insert(replacement)
427 self.editor.endUndoAction()
428 self.pos += len(replacement) - len(self.word)
429
430 def replaceAlways(self, replacement):
431 """
432 Public method to tell the checker to always replace the current word
433 with the replacement string.
434
435 @param replacement replacement string (string)
436 """
437 self.__replaceWords[self.word] = replacement
438 self.replace(replacement)
439
440 ##################################################################
441 ## Methods below implement the iterator protocol
442 ##################################################################
443
444 def __iter__(self):
445 """
446 Private method to create an iterator.
447 """
448 return self
449
450 def __next__(self):
451 """
452 Private method to advance to the next error.
453 """
454
455 def next(self):
456 """
457 Public method to advance to the next error.
458
459 @return self
460 """
461 spell = self._spelling_dict
462 if spell:
463 while self.pos < self.endPos and self.pos >= 0:
464 word, wordStart, wordEnd = self.__getNextWord(self.pos, self.endPos)
465 self.pos = wordEnd
466 if (wordEnd - wordStart) >= self.minimumWordSize and \
467 self.__checkRegion(wordStart):
468 if spell.check(word):
469 continue
470 if word in self.__ignoreWords:
471 continue
472 self.word = word
473 self.wordStart = wordStart
474 self.wordEnd = wordEnd
475 if word in self.__replaceWords:
476 self.replace(self.__replaceWords[word])
477 continue
478 return self
479
480 raise StopIteration

eric ide

mercurial