QScintilla/SpellChecker.py

changeset 0
de9c2efb9d02
child 6
52e8c820d0dd
diff -r 000000000000 -r de9c2efb9d02 QScintilla/SpellChecker.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/QScintilla/SpellChecker.py	Mon Dec 28 16:03:33 2009 +0000
@@ -0,0 +1,480 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2008 - 2009 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the spell checker for the editor component.
+
+The spell checker is based on pyenchant.
+"""
+
+try:
+    import enchant
+except (ImportError, AttributeError, OSError):
+    pass
+
+import os
+
+from PyQt4.QtCore import *
+
+import Preferences
+import Utilities
+
+class SpellChecker(QObject):
+    """
+    Class implementing a pyenchant based spell checker.
+    """
+    # class attributes to be used as defaults
+    _spelling_lang = None
+    _spelling_dict = None
+    
+    def __init__(self, editor, indicator, defaultLanguage = None, checkRegion = None):
+        """
+        Constructor
+        
+        @param editor reference to the editor object (QScintilla.Editor)
+        @param indicator spell checking indicator
+        @keyparam defaultLanguage the language to be used as the default (string).
+            The string should be in language locale format (e.g. en_US, de).
+        @keyparam checkRegion reference to a function to check for a valid region
+        """
+        self.editor = editor
+        self.indicator = indicator
+        if defaultLanguage is not None:
+            self.setDefaultLanguage(defaultLanguage)
+        if checkRegion is not None:
+            self.__checkRegion = checkRegion
+        else:
+            self.__checkRegion = lambda r: True
+        self.minimumWordSize = 3
+        self.lastCheckedLine = -1
+        
+        self.__ignoreWords = []
+        self.__replaceWords = {}
+    
+    @classmethod
+    def getAvailableLanguages(cls):
+        """
+        Public classmethod to get all available languages.
+        
+        @return list of available languages (list of strings)
+        """
+        try:
+            return enchant.list_languages()
+        except NameError:
+            pass
+        return []
+    
+    @classmethod
+    def isAvailable(cls):
+        """
+        Public classmethod to check, if spellchecking is available.
+        
+        @return flag indicating availability (boolean)
+        """
+        if Preferences.getEditor("SpellCheckingEnabled"):
+            try:
+                return len(enchant.list_languages()) > 0
+            except NameError:
+                pass
+        return False
+    
+    @classmethod
+    def _getDict(cls, lang, pwl = "", pel = ""):
+        """
+        Protected classmethod to get a new dictionary.
+        
+        @param lang the language to be used as the default (string).
+            The string should be in language locale format (e.g. en_US, de).
+        @keyparam pwl name of the personal/project word list (string)
+        @keyparam pel name of the personal/project exclude list (string)
+        @return reference to the dictionary (enchant.Dict)
+        """
+        if not pwl:
+            pwl = unicode(Preferences.getEditor("SpellCheckingPersonalWordList"))
+            if not pwl:
+                pwl = os.path.join(Utilities.getConfigDir(), "spelling", "pwl.dic")
+            d = os.path.dirname(pwl)
+            if not os.path.exists(d):
+                os.makedirs(d)
+        
+        if not pel:
+            pel = unicode(Preferences.getEditor("SpellCheckingPersonalExcludeList"))
+            if not pel:
+                pel = os.path.join(Utilities.getConfigDir(), "spelling", "pel.dic")
+            d = os.path.dirname(pel)
+            if not os.path.exists(d):
+                os.makedirs(d)
+        
+        try:
+            d = enchant.DictWithPWL(lang, pwl, pel)
+        except:
+            # Catch all exceptions, because if pyenchant isn't available, you
+            # can't catch the enchant.DictNotFound error.
+            d = None
+        return d
+    
+    @classmethod
+    def setDefaultLanguage(cls, language):
+        """
+        Public classmethod to set the default language.
+        
+        @param language the language to be used as the default (string).
+            The string should be in language locale format (e.g. en_US, de).
+        """
+        cls._spelling_lang = language
+        cls._spelling_dict = cls._getDict(language)
+    
+    def setLanguage(self, language, pwl = "", pel = ""):
+        """
+        Public method to set the current language.
+        
+        @param language the language to be used as the default (string).
+            The string should be in language locale format (e.g. en_US, de).
+        @keyparam pwl name of the personal/project word list (string)
+        @keyparam pel name of the personal/project exclude list (string)
+        """
+        self._spelling_lang = language
+        self._spelling_dict = self._getDict(language, pwl = pwl, 
+                                            pel = pel)
+    
+    def getLanguage(self):
+        """
+        Public method to get the current language.
+        
+        @return current language in language locale format (string)
+        """
+        return self._spelling_lang
+    
+    def setMinimumWordSize(self, size):
+        """
+        Public method to set the minimum word size.
+        
+        @param size minimum word size (integer)
+        """
+        self.minimumWordSize = size
+    
+    def __getNextWord(self, pos, endPosition):
+        """
+        Private method to get the next word in the text after the given position.
+        
+        @param pos position to start word extraction (integer)
+        @param endPosition position to stop word extraction (integer)
+        @return tuple of three values (the extracted word (string), 
+            start position (integer), end position (integer))
+        """
+        if pos < 0 or pos >= endPosition:
+            return "", -1, -1
+        
+        ch = self.editor.charAt(pos)
+        # 1. skip non-word characters
+        while pos < endPosition and not ch.isalnum():
+            pos = self.editor.positionAfter(pos)
+            ch = self.editor.charAt(pos)
+        if pos == endPosition:
+            return "",  -1, -1
+        startPos = pos
+        
+        # 2. extract the word
+        word = ""
+        while pos < endPosition and ch.isalnum():
+            word += ch
+            pos = self.editor.positionAfter(pos)
+            ch = self.editor.charAt(pos)
+        endPos = pos
+        if word.isdigit():
+            return self.__getNextWord(endPos, endPosition)
+        else:
+            return word, startPos, endPos
+    
+    def getContext(self, wordStart, wordEnd):
+        """
+        Public method to get the context of a faulty word.
+        
+        @param wordStart the starting position of the word (integer)
+        @param wordEnd the ending position of the word (integer)
+        @return tuple of the leading and trailing context (string, string)
+        """
+        sline, sindex = self.editor.lineIndexFromPosition(wordStart)
+        eline, eindex = self.editor.lineIndexFromPosition(wordEnd)
+        text = self.editor.text(sline)
+        return (text[:sindex], text[eindex:])
+    
+    def getError(self):
+        """
+        Public method to get information about the last error found.
+        
+        @return tuple of last faulty word (string), starting position of the
+            faulty word (integer) and ending position of the faulty word (integer)
+        """
+        return (self.word, self.wordStart, self.wordEnd)
+    
+    def initCheck(self, startPos, endPos):
+        """
+        Public method to initialize a spell check.
+        
+        @param startPos position to start at (integer)
+        @param endPos position to end at (integer)
+        @return flag indicating successful initialization (boolean)
+        """
+        if startPos == endPos:
+            return False
+        
+        spell = self._spelling_dict
+        if spell is None:
+            return False
+        
+        self.editor.clearIndicatorRange(self.indicator, startPos, endPos - startPos)
+        
+        self.pos = startPos
+        self.endPos = endPos
+        self.word = ""
+        self.wordStart = -1
+        self.wordEnd = -1
+        return True
+    
+    def __checkDocumentPart(self, startPos, endPos):
+        """
+        Private method to check some part of the document.
+        
+        @param startPos position to start at (integer)
+        @param endPos position to end at (integer)
+        """
+        if not self.initCheck(startPos,  endPos):
+            return
+        
+        while True:
+            try:
+                self.next()
+                self.editor.setIndicatorRange(self.indicator, self.wordStart, 
+                                              self.wordEnd - self.wordStart)
+            except StopIteration:
+                break
+    
+    def __incrementalCheck(self):
+        """
+        Private method to check the document incrementally.
+        """
+        if self.lastCheckedLine < 0:
+            return
+        
+        linesChunk = Preferences.getEditor("AutoSpellCheckChunkSize")
+        self.checkLines(self.lastCheckedLine, self.lastCheckedLine + linesChunk)
+        self.lastCheckedLine = self.lastCheckedLine + linesChunk + 1
+        if self.lastCheckedLine >= self.editor.lines():
+            self.lastCheckedLine = -1
+        else:
+            QTimer.singleShot(0, self.__incrementalCheck)
+    
+    def checkWord(self, pos, atEnd = False):
+        """
+        Public method to check the word at position pos.
+        
+        @param pos position to check at (integer)
+        @keyparam atEnd flag indicating the position is at the end of the word
+            to check (boolean)
+        """
+        spell = self._spelling_dict
+        if spell is None:
+            return
+        
+        if atEnd:
+            pos = self.editor.positionBefore(pos)
+        
+        if pos >= 0 and self.__checkRegion(pos):
+            if not self.editor.charAt(pos).isalnum():
+                pos = self.editor.positionBefore(pos)
+            
+            if self.editor.charAt(pos).isalnum():
+                line, index = self.editor.lineIndexFromPosition(pos)
+                word = self.editor.getWord(line, index, useWordChars = False)
+                if len(word) >= self.minimumWordSize:
+                    ok = spell.check(word)
+                    start, end = \
+                        self.editor.getWordBoundaries(line, index, useWordChars = False)
+                    if ok:
+                        self.editor.clearIndicator(self.indicator, line, start, line, end)
+                    else:
+                        # spell check indicated an error
+                        self.editor.setIndicator(self.indicator, line, start, line, end)
+    
+    def checkLines(self, firstLine, lastLine):
+        """
+        Public method to check some lines of text.
+        
+        @param firstLine line number of first line to check (integer)
+        @param lastLine line number of last line to check (integer)
+        """
+        startPos = self.editor.positionFromLineIndex(firstLine, 0)
+        
+        if lastLine >= self.editor.lines():
+            lastLine = self.editor.lines() - 1
+        endPos = self.editor.lineEndPosition(lastLine)
+        
+        self.__checkDocumentPart(startPos, endPos)
+    
+    def checkDocument(self):
+        """
+        Public method to check the complete document
+        """
+        self.__checkDocumentPart(0, self.editor.length())
+    
+    def checkDocumentIncrementally(self):
+        """
+        Public method to check the document incrementally.
+        """
+        spell = self._spelling_dict
+        if spell is None:
+            return
+        
+        if Preferences.getEditor("AutoSpellCheckingEnabled"):
+            self.lastCheckedLine = 0
+            QTimer.singleShot(0, self.__incrementalCheck)
+    
+    def stopIncrementalCheck(self):
+        """
+        Public method to stop an incremental check.
+        """
+        self.lastCheckedLine = -1
+    
+    def checkSelection(self):
+        """
+        Private method to check the current selection.
+        """
+        selStartLine, selStartIndex, selEndLine, selEndIndex = \
+            self.editor.getSelection()
+        self.__checkDocumentPart(
+            self.editor.positionFromLineIndex(selStartLine, selStartIndex), 
+            self.editor.positionFromLineIndex(selEndLine, selEndIndex)
+        )
+    
+    def checkCurrentPage(self):
+        """
+        Private method to check the currently visible page.
+        """
+        startLine = self.editor.firstVisibleLine()
+        endLine = startLine + self.editor.linesOnScreen()
+        self.checkLines(startLine, endLine)
+    
+    def clearAll(self):
+        """
+        Public method to clear all spelling markers.
+        """
+        self.editor.clearIndicatorRange(self.indicator, 0, self.editor.length())
+    
+    def getSuggestions(self, word):
+        """
+        Public method to get suggestions for the given word.
+        
+        @param word word to get suggestions for (string)
+        @return list of suggestions (list of strings)
+        """
+        spell = self._spelling_dict
+        if spell and len(word) >= self.minimumWordSize:
+            suggestions = spell.suggest(word)
+            return suggestions
+        
+        return []
+    
+    def add(self, word = None):
+        """
+        Public method to add a word to the personal word list.
+        
+        @param word word to add (string)
+        """
+        spell = self._spelling_dict
+        if spell:
+            if word is None:
+                word = self.word
+            spell.add(word)
+    
+    def remove(self, word):
+        """
+        Public method to add a word to the personal exclude list.
+        
+        @param word word to add (string)
+        """
+        spell = self._spelling_dict
+        if spell:
+            spell.remove(word)
+    
+    def ignoreAlways(self, word = None):
+        """
+        Public method to tell the checker, to always ignore the given word
+        or the current word.
+        
+        @param word word to be ignored (string)
+        """
+        if word is None:
+            word = self.word
+        if word not in self.__ignoreWords:
+            self.__ignoreWords.append(word)
+    
+    def replace(self, replacement):
+        """
+        Public method to tell the checker to replace the current word with 
+        the replacement string.
+        
+        @param replacement replacement string (string)
+        """
+        sline, sindex = self.editor.lineIndexFromPosition(self.wordStart)
+        eline, eindex = self.editor.lineIndexFromPosition(self.wordEnd)
+        self.editor.setSelection(sline, sindex, eline, eindex)
+        self.editor.beginUndoAction()
+        self.editor.removeSelectedText()
+        self.editor.insert(replacement)
+        self.editor.endUndoAction()
+        self.pos += len(replacement) - len(self.word)
+    
+    def replaceAlways(self, replacement):
+        """
+        Public method to tell the checker to always replace the current word
+        with the replacement string.
+        
+        @param replacement replacement string (string)
+        """
+        self.__replaceWords[self.word] = replacement
+        self.replace(replacement)
+    
+    ##################################################################
+    ## Methods below implement the iterator protocol
+    ##################################################################
+    
+    def __iter__(self):
+        """
+        Private method to create an iterator.
+        """
+        return self
+    
+    def __next__(self):
+        """
+        Private method to advance to the next error.
+        """
+    
+    def next(self):
+        """
+        Public method to advance to the next error.
+        
+        @return self
+        """
+        spell = self._spelling_dict
+        if spell:
+            while self.pos < self.endPos and self.pos >= 0:
+                word, wordStart, wordEnd = self.__getNextWord(self.pos, self.endPos)
+                self.pos = wordEnd
+                if (wordEnd - wordStart) >= self.minimumWordSize and \
+                   self.__checkRegion(wordStart):
+                    if spell.check(word):
+                        continue
+                    if word in self.__ignoreWords:
+                        continue
+                    self.word = word
+                    self.wordStart = wordStart
+                    self.wordEnd = wordEnd
+                    if word in self.__replaceWords:
+                        self.replace(self.__replaceWords[word])
+                        continue
+                    return self
+        
+        raise StopIteration

eric ide

mercurial