eric6/QScintilla/SpellChecker.py

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

eric ide

mercurial