def parse(self, text): """ Parse some text, assuming it is in the form of: word 1414 {grammar} word 1414 {grammar} etc. and returns a list of tupples '(word, strong, grammar)'. """ result = [] r = QRegExp(r'([αβχδεφγηιϕκλμνοπθρστυςωξψζ]*)' + '\s*' + '([\d ]+)' + '\s*' + '\{(.*)\}\s*') r.setMinimal(True) pos = r.indexIn(text) while pos >= 0: # Some verbs have two strong numbers, we keep only the first, # the second one being grammar strong = r.cap(2).strip() if " " in strong: strong = strong.split(" ")[0] result.append((r.cap(1).strip(), strong, r.cap(3).strip())) pos = r.indexIn(text, pos + len(r.cap(0))) return result
def handle_stdout(self): """ Private slot to handle the readyReadStdout signal of the pylint process. """ result_list = [] #regex = QRegExp('(\w)\S*:\S*(\d*):.*: (.*)') regex = QRegExp('(\w)\s*:\s*(\d*):(.*)') regex_score = \ QRegExp('.*at.(\d.\d*)/10.*') while self.pylint_pross and self.pylint_pross.canReadLine(): result = unicode(self.pylint_pross.readLine()) if result != None: pos = 0 while True: pos = regex.indexIn(result, pos) if pos < 0: if regex_score.indexIn(result, 0) >= 0: self.win.setWindowTitle( \ "PyLint Results :" \ + str(regex_score.cap(1)) \ + ':' + os.path.basename(str(self.parent.editor.filename))) break result_list.append( (regex.cap(1), regex.cap(2), regex.cap(3))) #print 'Append : ',(regex.cap(1), regex.cap(2), regex.cap(3)) pos = pos + regex.matchedLength() if len(result_list) > 0: self.win.append_results(result_list)
def highlightBlock(self, text): for pattern, format in self.rules: exp = QRegExp(pattern) index = exp.indexIn(text) while index >= 0: length = exp.matchedLength() if exp.numCaptures() > 0: self.setFormat(exp.pos(1), len(str(exp.cap(1))), format) else: self.setFormat(exp.pos(0), len(str(exp.cap(0))), format) index = exp.indexIn(text, index + length) # Multi line strings start = self.multilineStart end = self.multilineEnd self.setCurrentBlockState(0) startIndex, skip = 0, 0 if self.previousBlockState() != 1: startIndex, skip = start.indexIn(text), 3 while startIndex >= 0: endIndex = end.indexIn(text, startIndex + skip) if endIndex == -1: self.setCurrentBlockState(1) commentLen = len(text) - startIndex else: commentLen = endIndex - startIndex + 3 self.setFormat(startIndex, commentLen, self.stringFormat) startIndex, skip = (start.indexIn(text, startIndex + commentLen + 3), 3)
def handle_stdout(self): """ Private slot to handle the readyReadStdout signal of the pylint process. """ result_list = [] #regex = QRegExp('(\w)\S*:\S*(\d*):.*: (.*)') regex = QRegExp('(\w)\s*:\s*(\d*):(.*)') regex_score = \ QRegExp('.*at.(\d.\d*)/10.*') while self.pylint_pross and self.pylint_pross.canReadLine(): result = unicode(self.pylint_pross.readLine()) if result != None: pos = 0 while True: pos = regex.indexIn(result, pos) if pos < 0: if regex_score.indexIn(result, 0) >= 0: self.win.setWindowTitle( \ "PyLint Results :" \ + str(regex_score.cap(1)) \ + ':' + os.path.basename(str(self.parent.editor.filename))) break result_list.append((regex.cap(1), regex.cap(2), regex.cap(3))) #print 'Append : ',(regex.cap(1), regex.cap(2), regex.cap(3)) pos = pos + regex.matchedLength() if len(result_list)>0: self.win.append_results(result_list)
class HoogleRunner(plasmascript.Runner): def init(self): """ Initialize and register with Plasma. """ self.myIcon = QIcon(self.package().filePath("images", "lambda.svg")) self.regExp = QRegExp("^hoogle (.*)$") syntax = Plasma.RunnerSyntax("hoogle :q:", "Query hoogle for :q:") self.addSyntax(syntax) def match(self, context): """ Add a match to the given `context` iff the query starts with 'hoogle'. """ if context.isValid() and self.regExp.exactMatch(context.query()): term = self.regExp.cap(1) m = Plasma.QueryMatch(self.runner) m.setText("Query Hoogle for '%s'" % term) m.setType(Plasma.QueryMatch.ExactMatch) m.setIcon(self.myIcon) m.setData(term) context.addMatch(term, m) def run(self, context, match): """ Have KDE open the query in the browser. """ urlString = QString("http://www.haskell.org/hoogle/?hoogle=") # Apparently Hoogle doesn't like percent encoded URLs. # urlString.append(QUrl.toPercentEncoding(match.data().toString())) urlString.append(match.data().toString()) QDesktopServices().openUrl(QUrl(urlString))
def highlightBlock(self, text): """Apply syntax highlighting to the given block of text.""" for expression, nth, format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = expression.cap(nth).length() self.setFormat(index, length, format) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline(text, *self.tri_single) if not in_multiline: in_multiline = self.match_multiline(text, *self.tri_double) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = expression.cap(0).length() self.setFormat(index, length, STYLES['spaces']) index = expression.indexIn(text, index + length)
def run(self): styles = {} self.msleep(300) block = self._highlighter.document().begin() while block.blockNumber() != -1: text = block.text() formats = [] for expression, nth, char_format in self._highlighter.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = expression.cap(nth).length() formats.append((index, length, char_format)) index = expression.indexIn(text, index + length) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = expression.cap(0).length() formats.append((index, length, STYLES['spaces'])) index = expression.indexIn(text, index + length) styles[block.blockNumber()] = formats block = block.next() self.emit(SIGNAL("highlightingDetected(PyQt_PyObject)"), styles)
def run(self): """Execute this rules in another thread to avoid blocking the ui.""" styles = {} self.msleep(300) block = self._highlighter.document().begin() while block.blockNumber() != -1: text = block.text() formats = [] for expression, nth, char_format in self._highlighter.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = len(expression.cap(nth)) formats.append((index, length, char_format)) index = expression.indexIn(text, index + length) # Spaces expression = QRegExp("\s+") index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = len(expression.cap(0)) formats.append((index, length, STYLES["spaces"])) index = expression.indexIn(text, index + length) styles[block.blockNumber()] = formats block = block.next() self.emit(SIGNAL("highlightingDetected(PyQt_PyObject)"), styles)
def run(self): self.msleep(300) block = self._highlighter.document().begin() while block.blockNumber() != -1: text = block.text() formats = [] for expression, nth, char_format in self._highlighter.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = expression.cap(nth).length() formats.append((index, length, char_format)) index = expression.indexIn(text, index + length) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = expression.cap(0).length() formats.append((index, length, STYLES['spaces'])) index = expression.indexIn(text, index + length) self.styles[block.blockNumber()] = formats block = block.next()
def run(self): """Execute this rules in another thread to avoid blocking the ui.""" styles = {} self.msleep(300) block = self._highlighter.document().begin() while block.blockNumber() != -1: text = block.text() formats = [] for expression, nth, char_format in self._highlighter.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = len(expression.cap(nth)) formats.append((index, length, char_format)) index = expression.indexIn(text, index + length) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = len(expression.cap(0)) formats.append((index, length, STYLES['spaces'])) index = expression.indexIn(text, index + length) styles[block.blockNumber()] = formats block = block.next() self.emit(SIGNAL("highlightingDetected(PyQt_PyObject)"), styles)
def run(self): self.msleep(300) block = self._highlighter.document().begin() while block.blockNumber() != -1: text = block.text() formats = [] for expression, nth, char_format in self._highlighter.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = expression.cap(nth).length() formats.append((index, length, char_format)) index = expression.indexIn(text, index + length) # Spaces expression = QRegExp("\s+") index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = expression.cap(0).length() formats.append((index, length, STYLES["spaces"])) index = expression.indexIn(text, index + length) self.styles[block.blockNumber()] = formats block = block.next()
def getIndent(self,text): spaces= QRegExp("^(\\s*).*$") #~ indentation="" #~ if len(text) > 0 and text[-1] in [':', '{', '(', '[']: #~ indentation=" " if spaces.indexIn(text) == -1: return "" return spaces.cap(1)
def parseBook(self, book): """ Takes a raw book and returns an array of array of array such as: [0][0][0] = Mt 1, 1 [1][8][4] = Mr 9, 5 i.e. [bookNumber][chapter][verse] Note that chapter and verse start from 0. etc. """ result = [] line = book.split("\n") # Puts each verse on one line verse = [] v = "" for l in line: if l[0:5] == " ": if v: verse.append(v) v = l[5:] else: v += " " + l if v: verse.append(v) r = QRegExp(r'(\d+):(\d+)\s*(.*)') chapter = [] for v in verse: pos = r.indexIn(v) chap = int(r.cap(1)) ver = int(r.cap(2)) text = r.cap(3).strip() # Variantes textuelles text = re.sub(r'\|( .* )\|( .* )\|', r'\1', text) if ver == 1: if len(chapter) > 0: result.append(chapter) chapter = [text] else: chapter.append(text) if len(chapter) > 0: result.append(chapter) return result
def getObject(self, row): val = self.data(self.index(row, 0), Qt.UserRole) fld = val if val is not None else self._getNewObject() fld.name = self.data(self.index(row, 0)) or "" typestr = self.data(self.index(row, 1)) or "" regex = QRegExp( "([^\(]+)\(([^\)]+)\)" ) startpos = regex.indexIn( typestr ) if startpos >= 0: fld.dataType = regex.cap(1).strip() fld.modifier = regex.cap(2).strip() else: fld.modifier = None fld.dataType = typestr fld.notNull = self.data(self.index(row, 2), Qt.CheckStateRole) == Qt.Unchecked fld.primaryKey = self.data(self.index(row, 1), Qt.UserRole) return fld
def getSpatialRefInfo(self, srid): if not self.has_spatial: return try: c = self._execute(None, "SELECT srtext FROM spatial_ref_sys WHERE srid = '%d'" % srid) except DbError as e: return sr = self._fetchone(c) self._close_cursor(c) if sr is None: return srtext = sr[0] # try to extract just SR name (should be quoted in double quotes) regex = QRegExp('"([^"]+)"') if regex.indexIn(srtext) > -1: srtext = regex.cap(1) return srtext
def __init__(self, row, table): TableField.__init__(self, table) self.num, self.name, self.dataType, self.charMaxLen, self.modifier, self.notNull, self.hasDefault, self.default, typeStr = row self.primaryKey = False # get modifier (e.g. "precision,scale") from formatted type string trimmedTypeStr = typeStr.strip() regex = QRegExp("\((.+)\)$") startpos = regex.indexIn(trimmedTypeStr) if startpos >= 0: self.modifier = regex.cap(1).strip() else: self.modifier = None # find out whether fields are part of primary key for con in self.table().constraints(): if con.type == TableConstraint.TypePrimaryKey and self.num in con.columns: self.primaryKey = True break
def getObject(self, row): val = self.data(self.index(row, 0), Qt.UserRole) fld = val if val is not None else self._getNewObject() fld.name = self.data(self.index(row, 0)) or "" typestr = self.data(self.index(row, 1)) or "" regex = QRegExp("([^\(]+)\(([^\)]+)\)") startpos = regex.indexIn(typestr) if startpos >= 0: fld.dataType = regex.cap(1).strip() fld.modifier = regex.cap(2).strip() else: fld.modifier = None fld.dataType = typestr fld.notNull = self.data(self.index(row, 2), Qt.CheckStateRole) == Qt.Unchecked fld.primaryKey = self.data(self.index(row, 1), Qt.UserRole) return fld
class MacroHighlighter(QSyntaxHighlighter): def __init__(self, textboxdoc, valid_list_of_commands): QSyntaxHighlighter.__init__(self, textboxdoc) self.valid_syntax = "|".join( [command.regexp_str for command in valid_list_of_commands]) self.my_expression = QRegExp(self.valid_syntax) #define a blue font format for valid commands self.valid = QTextCharFormat() self.valid.setForeground(Qt.black) #define a bold red font format for invalid commands self.invalid = QTextCharFormat() self.invalid.setFontWeight(QFont.Bold) self.invalid.setForeground(Qt.red) #define a blue font format for valid parameters self.valid_value = QTextCharFormat() self.valid_value.setFontWeight(QFont.Bold) #self.valid_value.setForeground(QColor.fromRgb(255,85,0)) self.valid_value.setForeground(Qt.blue) def highlightBlock(self, text): #this function is automatically called when some text is changed #in the texbox. 'text' is the line of text where the change occured #check if the line of text contains a valid command match = self.my_expression.exactMatch(text) if match: #valid command found: highlight the command in blue self.setFormat(0, len(text), self.valid) #highlight the parameters in orange #loop on all the parameters that can be captured for i in range(self.my_expression.captureCount()): #if a parameter was captured, it's position in the text will be >=0 and its capture contains some value 'xxx' #otherwise its position is -1 and its capture contains an empty string '' if self.my_expression.pos(i + 1) != -1: self.setFormat(self.my_expression.pos(i + 1), len(self.my_expression.cap(i + 1)), self.valid_value) else: #no valid command found: highlight in red self.setFormat(0, len(text), self.invalid)
class MacroHighlighter(QSyntaxHighlighter): def __init__(self,textboxdoc,valid_list_of_commands): QSyntaxHighlighter.__init__(self,textboxdoc) self.valid_syntax="|".join([command.regexp_str for command in valid_list_of_commands]) self.my_expression = QRegExp(self.valid_syntax) #define a blue font format for valid commands self.valid = QTextCharFormat() self.valid.setForeground(Qt.black) #define a bold red font format for invalid commands self.invalid = QTextCharFormat() self.invalid.setFontWeight(QFont.Bold) self.invalid.setForeground(Qt.red) #define a blue font format for valid parameters self.valid_value=QTextCharFormat() self.valid_value.setFontWeight(QFont.Bold) #self.valid_value.setForeground(QColor.fromRgb(255,85,0)) self.valid_value.setForeground(Qt.blue) def highlightBlock(self, text): #this function is automatically called when some text is changed #in the texbox. 'text' is the line of text where the change occured #check if the line of text contains a valid command match = self.my_expression.exactMatch(text) if match: #valid command found: highlight the command in blue self.setFormat(0, len(text), self.valid) #highlight the parameters in orange #loop on all the parameters that can be captured for i in range(self.my_expression.captureCount()): #if a parameter was captured, it's position in the text will be >=0 and its capture contains some value 'xxx' #otherwise its position is -1 and its capture contains an empty string '' if self.my_expression.pos(i+1)!=-1: self.setFormat(self.my_expression.pos(i+1), len(self.my_expression.cap(i+1)), self.valid_value) else: #no valid command found: highlight in red self.setFormat(0, len(text), self.invalid)
class Highlighter(QSyntaxHighlighter): """Syntax Highlighter for NINJA-IDE.""" # braces braces = ["\\(", "\\)", "\\{", "\\}", "\\[", "\\]"] def __init__(self, document, lang=None, scheme=None, errors=None, pep8=None, migration=None): QSyntaxHighlighter.__init__(self, document) self.highlight_function = self.realtime_highlight self.errors = errors self.pep8 = pep8 self.migration = migration self.selected_word_lines = [] self.visible_limits = (0, 50) self._styles = {} if lang is not None: self.apply_highlight(lang, scheme) def sanitize(self, word): """Sanitize the string to avoid problems with the regex.""" return word.replace("\\", "\\\\") def apply_highlight(self, lang, scheme=None, syntax=None): """Set the rules that will decide what to highlight and how.""" if syntax is None: langSyntax = settings.SYNTAX.get(lang, {}) else: langSyntax = syntax if scheme is not None: restyle(scheme) keywords = langSyntax.get("keywords", []) operators = langSyntax.get("operators", []) extras = langSyntax.get("extras", []) rules = [] # Keyword, operator, brace and extras rules keyword_pattern = "(^|[^\w\.]{1})(%s)([^\w]{1}|$)" rules += [(keyword_pattern % w, 2, STYLES["keyword"]) for w in keywords] rules += [(r"%s" % o, 0, STYLES["operator"]) for o in operators] rules += [(r"%s" % b, 0, STYLES["brace"]) for b in Highlighter.braces] rules += [(keyword_pattern % e, 2, STYLES["extras"]) for e in extras] # All other rules proper = langSyntax.get("properObject", None) if proper is not None: proper = r"\b%s\b" % str(proper[0]) rules += [(proper, 0, STYLES["properObject"])] rules.append((r"__\w+__", 0, STYLES["properObject"])) # Classes and functions definition = langSyntax.get("definition", []) for de in definition: expr = r"\b%s\b\s*(\w+)" % de rules.append((expr, 1, STYLES["definition"])) # Numeric literals rules += [ (r"\b[+-]?[0-9]+[lL]?\b", 0, STYLES["numbers"]), (r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", 0, STYLES["numbers"]), (r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b", 0, STYLES["numbers"]), ] # Regular expressions regex = langSyntax.get("regex", []) for reg in regex: expr = reg[0] color = resources.COLOR_SCHEME["extras"] style = "" if len(reg) > 1: if reg[1] in resources.CUSTOM_SCHEME: color = resources.CUSTOM_SCHEME[reg[1]] elif reg[1] in resources.COLOR_SCHEME: color = resources.COLOR_SCHEME[reg[1]] if len(reg) > 2: style = reg[2] rules.append((expr, 0, format(color, style))) # Strings stringChar = langSyntax.get("string", []) for sc in stringChar: expr = r'"[^"\\]*(\\.[^"\\]*)*"' if sc == '"' else r"'[^'\\]*(\\.[^'\\]*)*'" rules.append((expr, 0, STYLES["string"])) # Comments comments = langSyntax.get("comment", []) for co in comments: expr = co + "[^\\n]*" rules.append((expr, 0, STYLES["comment"])) # Multi-line strings (expression, flag, style) # FIXME: The triple-quotes in these two lines will mess up the # syntax highlighting from this point onward self.tri_single = (QRegExp("'''"), 1, STYLES["string2"]) self.tri_double = (QRegExp('"""'), 2, STYLES["string2"]) multi = langSyntax.get("multiline_comment", []) if multi: self.multi_start = (QRegExp(multi["open"]), STYLES["comment"]) self.multi_end = (QRegExp(multi["close"]), STYLES["comment"]) else: self.multi_start = None # Build a QRegExp for each pattern self.rules = [(QRegExp(pat), index, fmt) for (pat, index, fmt) in rules] self.selected_word_pattern = None # Apply Highlight to the document... (when colors change) self.rehighlight() def set_selected_word(self, word, partial=True): """Set the word to highlight.""" # partial = True for new highlighter compatibility if len(word) > 2: self.selected_word_pattern = QRegExp(r"\b%s\b" % self.sanitize(word)) else: self.selected_word_pattern = None def __highlight_pep8(self, char_format, user_data): """Highlight the lines with pep8 errors.""" user_data.error = True char_format = char_format.toCharFormat() char_format.setUnderlineColor( QColor(resources.CUSTOM_SCHEME.get("pep8-underline", resources.COLOR_SCHEME["pep8-underline"])) ) char_format.setUnderlineStyle(QTextCharFormat.WaveUnderline) return char_format def __highlight_lint(self, char_format, user_data): """Highlight the lines with lint errors.""" user_data.error = True char_format = char_format.toCharFormat() char_format.setUnderlineColor( QColor(resources.CUSTOM_SCHEME.get("error-underline", resources.COLOR_SCHEME["error-underline"])) ) char_format.setUnderlineStyle(QTextCharFormat.WaveUnderline) return char_format def __highlight_migration(self, char_format, user_data): """Highlight the lines with lint errors.""" user_data.error = True char_format = char_format.toCharFormat() char_format.setUnderlineColor( QColor(resources.CUSTOM_SCHEME.get("migration-underline", resources.COLOR_SCHEME["migration-underline"])) ) char_format.setUnderlineStyle(QTextCharFormat.WaveUnderline) return char_format def highlightBlock(self, text): """Apply syntax highlighting to the given block of text.""" self.highlight_function(text) def set_open_visible_area(self, is_line, position): """Set the range of lines that should be highlighted on open.""" if is_line: self.visible_limits = (position - 50, position + 50) def open_highlight(self, text): """Only highlight the lines inside the accepted range.""" if self.visible_limits[0] <= self.currentBlock().blockNumber() <= self.visible_limits[1]: self.realtime_highlight(text) else: self.setCurrentBlockState(0) def async_highlight(self): """Execute a thread to collect the info of the things to highlight. The thread will collect the data from where to where to highlight, and which kind of highlight to use for those sections, and return that info to the main thread after it process all the file.""" self.thread_highlight = HighlightParserThread(self) self.connect( self.thread_highlight, SIGNAL("highlightingDetected(PyQt_PyObject)"), self._execute_threaded_highlight ) self.thread_highlight.start() def _execute_threaded_highlight(self, styles=None): """Function called with the info collected when the thread ends.""" self.highlight_function = self.threaded_highlight if styles: self._styles = styles lines = list(set(styles.keys()) - set(range(self.visible_limits[0], self.visible_limits[1]))) # Highlight the rest of the lines that weren't highlighted on open self.rehighlight_lines(lines, False) else: self._styles = {} self.highlight_function = self.realtime_highlight def threaded_highlight(self, text): """Highlight each line using the info collected by the thread. This function doesn't need to execute the regular expressions to see where the highlighting starts and end for each rule, it just take the start and end point, and the proper highlighting style from the info returned from the thread and applied that to the document.""" hls = [] block = self.currentBlock() user_data = block.userData() if user_data is None: user_data = SyntaxUserData(False) user_data.clear_data() block_number = block.blockNumber() highlight_errors = lambda cf, ud: cf if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 elif self.migration and (block_number in self.migration.migration_data): highlight_errors = self.__highlight_migration char_format = block.charFormat() char_format = highlight_errors(char_format, user_data) self.setFormat(0, len(block.text()), char_format) block_styles = self._styles.get(block.blockNumber(), ()) for index, length, char_format in block_styles: char_format = highlight_errors(char_format, user_data) if self.format(index) != STYLES["string"]: self.setFormat(index, length, char_format) if char_format == STYLES["string"]: hls.append((index, index + length)) user_data.add_str_group(index, index + length) elif char_format == STYLES["comment"]: user_data.comment_start_at(index) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline( text, *self.tri_single, hls=hls, highlight_errors=highlight_errors, user_data=user_data ) if not in_multiline: in_multiline = self.match_multiline( text, *self.tri_double, hls=hls, highlight_errors=highlight_errors, user_data=user_data ) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) block.setUserData(user_data) def realtime_highlight(self, text): """Highlight each line while it is being edited. This function apply the proper highlight to the line being edited by the user, this is a really fast process for each line once you already have the document highlighted, but slow to do it the first time to highlight all the lines together.""" hls = [] block = self.currentBlock() user_data = block.userData() if user_data is None: user_data = SyntaxUserData(False) user_data.clear_data() block_number = block.blockNumber() highlight_errors = lambda cf, ud: cf if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 elif self.migration and (block_number in self.migration.migration_data): highlight_errors = self.__highlight_migration char_format = block.charFormat() char_format = highlight_errors(char_format, user_data) self.setFormat(0, len(block.text()), char_format) for expression, nth, char_format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = len(expression.cap(nth)) char_format = highlight_errors(char_format, user_data) if self.format(index) != STYLES["string"]: self.setFormat(index, length, char_format) if char_format == STYLES["string"]: hls.append((index, index + length)) user_data.add_str_group(index, index + length) elif char_format == STYLES["comment"]: user_data.comment_start_at(index) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline( text, *self.tri_single, hls=hls, highlight_errors=highlight_errors, user_data=user_data ) if not in_multiline: in_multiline = self.match_multiline( text, *self.tri_double, hls=hls, highlight_errors=highlight_errors, user_data=user_data ) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) # Highlight selected word if self.selected_word_pattern is not None: index = self.selected_word_pattern.indexIn(text, 0) while index >= 0: index = self.selected_word_pattern.pos(0) length = len(self.selected_word_pattern.cap(0)) char_format = self.format(index) color = STYLES["selectedWord"].foreground().color() color.setAlpha(100) char_format.setBackground(color) self.setFormat(index, length, char_format) index = self.selected_word_pattern.indexIn(text, index + length) # Spaces expression = QRegExp("\s+") index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = len(expression.cap(0)) char_format = STYLES["spaces"] char_format = highlight_errors(char_format, user_data) self.setFormat(index, length, char_format) index = expression.indexIn(text, index + length) block.setUserData(user_data) def _rehighlight_lines(self, lines): """If the document is valid, highlight the list of lines received.""" if self.document() is None: return for line in lines: block = self.document().findBlockByNumber(line) self.rehighlightBlock(block) def _get_errors_lines(self): """Return the number of lines that contains errors to highlight.""" errors_lines = [] block = self.document().begin() while block.isValid(): user_data = block.userData() if (user_data is not None) and (user_data.error): errors_lines.append(block.blockNumber()) block = block.next() return errors_lines def rehighlight_lines(self, lines, errors=True): """Rehighlight the lines for errors or selected words.""" if errors: errors_lines = self._get_errors_lines() refresh_lines = set(lines + errors_lines) else: refresh_lines = set(lines + self.selected_word_lines) self.selected_word_lines = lines self._rehighlight_lines(refresh_lines) def match_multiline(self, text, delimiter, in_state, style, hls=[], highlight_errors=lambda x: x, user_data=None): """Do highlighting of multi-line strings. ``delimiter`` should be a ``QRegExp`` for triple-single-quotes or triple-double-quotes, and ``in_state`` should be a unique integer to represent the corresponding state changes when inside those strings. Returns True if we're still inside a multi-line string when this function is finished. """ # If inside triple-single quotes, start at 0 if self.previousBlockState() == in_state: start = 0 add = 0 # Otherwise, look for the delimiter on this line else: start = delimiter.indexIn(text) # Move past this match add = delimiter.matchedLength() # As long as there's a delimiter match on this line... while start >= 0: # Look for the ending delimiter end = delimiter.indexIn(text, start + add) # Ending delimiter on this line? if end >= add: length = end - start + add + delimiter.matchedLength() self.setCurrentBlockState(0) # No; multi-line string else: self.setCurrentBlockState(in_state) length = len(text) - start + add st_fmt = self.format(start) start_collides = [pos for pos in hls if pos[0] < start < pos[1]] # Apply formatting if ( (st_fmt != STYLES["comment"]) or ((st_fmt == STYLES["comment"]) and (self.previousBlockState() != 0)) ) and (len(start_collides) == 0): if user_data is not None: style = highlight_errors(style, user_data) self.setFormat(start, length, style) else: self.setCurrentBlockState(0) # Look for the next match start = delimiter.indexIn(text, start + length) # Return True if still inside a multi-line string, False otherwise if self.currentBlockState() == in_state: return True else: return False def comment_multiline(self, text, delimiter_end, delimiter_start, style): """Process the beggining and end of a multiline comment.""" startIndex = 0 if self.previousBlockState() != 1: startIndex = delimiter_start.indexIn(text) while startIndex >= 0: endIndex = delimiter_end.indexIn(text, startIndex) commentLength = 0 if endIndex == -1: self.setCurrentBlockState(1) commentLength = len(text) - startIndex else: commentLength = endIndex - startIndex + delimiter_end.matchedLength() self.setFormat(startIndex, commentLength, style) startIndex = delimiter_start.indexIn(text, startIndex + commentLength)
def extractSqlQuery( self , source): rx = QRegExp('\\(select row_number\\(\\) over\\(order by 1\\) id, q.\\w+ from \\((.*)\\) as q\\)') if rx.indexIn(source) == -1: return 'Table: <b>' + source + '</b>' return 'SqlQuery: <b>' + rx.cap(1) + '</b>'
class Highlighter(QSyntaxHighlighter): # braces braces = ['\\(', '\\)', '\\{', '\\}', '\\[', '\\]'] def __init__(self, document, lang=None, scheme=None, errors=None, pep8=None): QSyntaxHighlighter.__init__(self, document) self.highlight_function = self.realtime_highlight self.errors = errors self.pep8 = pep8 self.selected_word_lines = [] self.visible_limits = (0, 50) if lang is not None: self.apply_highlight(lang, scheme) def sanitize(self, word): return word.replace('\\', '\\\\') def apply_highlight(self, lang, scheme=None, syntax=None): if syntax is None: langSyntax = settings.SYNTAX.get(lang, {}) else: langSyntax = syntax if scheme is not None: restyle(scheme) keywords = langSyntax.get('keywords', []) operators = langSyntax.get('operators', []) extras = langSyntax.get('extras', []) rules = [] # Keyword, operator, brace and extras rules keyword_pattern = '(^|[^\w\.]{1})(%s)([^\w]{1}|$)' rules += [(keyword_pattern % w, 2, STYLES['keyword']) for w in keywords] rules += [(r'%s' % o, 0, STYLES['operator']) for o in operators] rules += [(r'%s' % b, 0, STYLES['brace']) for b in Highlighter.braces] rules += [(keyword_pattern % e, 2, STYLES['extras']) for e in extras] # All other rules proper = langSyntax.get('properObject', None) if proper is not None: proper = r'\b%s\b' % str(proper[0]) rules += [(proper, 0, STYLES['properObject'])] rules.append((r'__\w+__', 0, STYLES['properObject'])) definition = langSyntax.get('definition', []) for de in definition: expr = r'\b%s\b\s*(\w+)' % de rules.append((expr, 1, STYLES['definition'])) # Numeric literals rules += [ (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), ] regex = langSyntax.get('regex', []) for reg in regex: expr = reg[0] color = resources.COLOR_SCHEME['extras'] style = '' if len(reg) > 1: if reg[1] in resources.CUSTOM_SCHEME: color = resources.CUSTOM_SCHEME[reg[1]] elif reg[1] in resources.COLOR_SCHEME: color = resources.COLOR_SCHEME[reg[1]] if len(reg) > 2: style = reg[2] rules.append((expr, 0, format(color, style))) stringChar = langSyntax.get('string', []) for sc in stringChar: expr = r'"[^"\\]*(\\.[^"\\]*)*"' if sc == '"' \ else r"'[^'\\]*(\\.[^'\\]*)*'" rules.append((expr, 0, STYLES['string'])) comments = langSyntax.get('comment', []) for co in comments: expr = co + '[^\\n]*' rules.append((expr, 0, STYLES['comment'])) # Multi-line strings (expression, flag, style) # FIXME: The triple-quotes in these two lines will mess up the # syntax highlighting from this point onward self.tri_single = (QRegExp("'''"), 1, STYLES["string2"]) self.tri_double = (QRegExp('"""'), 2, STYLES['string2']) multi = langSyntax.get('multiline_comment', []) if multi: self.multi_start = (QRegExp(multi['open']), STYLES['comment']) self.multi_end = (QRegExp(multi['close']), STYLES['comment']) else: self.multi_start = None # Build a QRegExp for each pattern self.rules = [(QRegExp(pat), index, fmt) for (pat, index, fmt) in rules] self.selected_word_pattern = None #Apply Highlight to the document... (when colors change) self.rehighlight() def set_selected_word(self, word): """Set the word to highlight.""" if len(word) > 2: self.selected_word_pattern = QRegExp( r'\b%s\b' % self.sanitize(word)) else: self.selected_word_pattern = None def __highlight_pep8(self, char_format, block): """Highlight the lines with errors.""" block.setUserData(SyntaxUserData(True)) char_format = char_format.toCharFormat() char_format.setUnderlineColor(QColor( resources.CUSTOM_SCHEME.get('pep8-underline', resources.COLOR_SCHEME['pep8-underline']))) char_format.setUnderlineStyle( QTextCharFormat.WaveUnderline) return char_format def __highlight_lint(self, char_format, block): """Highlight the lines with errors.""" block.setUserData(SyntaxUserData(True)) char_format = char_format.toCharFormat() char_format.setUnderlineColor(QColor( resources.CUSTOM_SCHEME.get('error-underline', resources.COLOR_SCHEME['error-underline']))) char_format.setUnderlineStyle( QTextCharFormat.WaveUnderline) return char_format def __clean_error(self, char_format, block): block.setUserData(SyntaxUserData()) return char_format def highlightBlock(self, text): """Apply syntax highlighting to the given block of text.""" self.highlight_function(text) def set_open_visible_area(self, is_line, position): if is_line: self.visible_limits = (position - 50, position + 50) def open_highlight(self, text): if self.visible_limits[0] <= self.currentBlock().blockNumber() <= \ self.visible_limits[1]: self.realtime_highlight(text) else: self.setCurrentBlockState(0) def async_highlight(self): self.thread_highlight = HighlightParserThread(self) self.connect(self.thread_highlight, SIGNAL("finished()"), self._execute_threaded_highlight) self.thread_highlight.start() def _execute_threaded_highlight(self): self.highlight_function = self.threaded_highlight if self.thread_highlight and self.thread_highlight.styles: lines = list(set(self.thread_highlight.styles.keys()) - set(range(self.visible_limits[0], self.visible_limits[1]))) self.rehighlight_lines(lines, False) self.thread_highlight = None self.highlight_function = self.realtime_highlight def threaded_highlight(self, text): hls = [] block = self.currentBlock() block_number = block.blockNumber() highlight_errors = self.__clean_error if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 char_format = block.charFormat() char_format = highlight_errors(char_format, block) self.setFormat(0, len(block.text()), char_format) styles = self.thread_highlight.styles.get(block.blockNumber(), ()) for index, length, char_format in styles: char_format = highlight_errors(char_format, block) if (self.format(index) != STYLES['string']): self.setFormat(index, length, char_format) if char_format == STYLES['string']: hls.append((index, index + length)) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline(text, *self.tri_single, hls=hls, highlight_errors=highlight_errors) if not in_multiline: in_multiline = self.match_multiline(text, *self.tri_double, hls=hls, highlight_errors=highlight_errors) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) def realtime_highlight(self, text): hls = [] block = self.currentBlock() block_number = block.blockNumber() highlight_errors = self.__clean_error if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 char_format = block.charFormat() char_format = highlight_errors(char_format, block) self.setFormat(0, len(block.text()), char_format) for expression, nth, char_format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = expression.cap(nth).length() char_format = highlight_errors(char_format, block) if (self.format(index) != STYLES['string']): self.setFormat(index, length, char_format) if char_format == STYLES['string']: hls.append((index, index + length)) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline(text, *self.tri_single, hls=hls, highlight_errors=highlight_errors) if not in_multiline: in_multiline = self.match_multiline(text, *self.tri_double, hls=hls, highlight_errors=highlight_errors) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) #Highlight selected word if self.selected_word_pattern is not None: index = self.selected_word_pattern.indexIn(text, 0) while index >= 0: index = self.selected_word_pattern.pos(0) length = self.selected_word_pattern.cap(0).length() char_format = self.format(index) color = QColor() color.setNamedColor(STYLES['selectedWord']) color.setAlpha(100) char_format.setBackground(color) self.setFormat(index, length, char_format) index = self.selected_word_pattern.indexIn( text, index + length) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = expression.cap(0).length() char_format = STYLES['spaces'] if settings.HIGHLIGHT_WHOLE_LINE: char_format = highlight_errors(char_format, block) self.setFormat(index, length, char_format) index = expression.indexIn(text, index + length) def _rehighlight_lines(self, lines): for line in lines: block = self.document().findBlockByNumber(line) self.rehighlightBlock(block) def _get_errors_lines(self): errors_lines = [] block = self.document().begin() while block.isValid(): user_data = block.userData() if (user_data is not None) and (user_data.error == True): errors_lines.append(block.blockNumber()) block = block.next() return errors_lines def rehighlight_lines(self, lines, errors=True): if errors: errors_lines = self._get_errors_lines() refresh_lines = set(lines + errors_lines) else: refresh_lines = set(lines + self.selected_word_lines) self.selected_word_lines = lines self._rehighlight_lines(refresh_lines) def match_multiline(self, text, delimiter, in_state, style, hls=[], highlight_errors=lambda x: x): """Do highlighting of multi-line strings. ``delimiter`` should be a ``QRegExp`` for triple-single-quotes or triple-double-quotes, and ``in_state`` should be a unique integer to represent the corresponding state changes when inside those strings. Returns True if we're still inside a multi-line string when this function is finished. """ # If inside triple-single quotes, start at 0 if self.previousBlockState() == in_state: start = 0 add = 0 # Otherwise, look for the delimiter on this line else: start = delimiter.indexIn(text) # Move past this match add = delimiter.matchedLength() # As long as there's a delimiter match on this line... while start >= 0: # Look for the ending delimiter end = delimiter.indexIn(text, start + add) # Ending delimiter on this line? if end >= add: length = end - start + add + delimiter.matchedLength() self.setCurrentBlockState(0) # No; multi-line string else: self.setCurrentBlockState(in_state) length = text.length() - start + add st_fmt = self.format(start) start_collides = [pos for pos in hls if pos[0] < start < pos[1]] # Apply formatting if ((st_fmt != STYLES['comment']) or \ ((st_fmt == STYLES['comment']) and (self.previousBlockState() != 0))) and \ (len(start_collides) == 0): style = highlight_errors(style, self.currentBlock()) self.setFormat(start, length, style) else: self.setCurrentBlockState(0) # Look for the next match start = delimiter.indexIn(text, start + length) # Return True if still inside a multi-line string, False otherwise if self.currentBlockState() == in_state: return True else: return False def comment_multiline(self, text, delimiter_end, delimiter_start, style): startIndex = 0 if self.previousBlockState() != 1: startIndex = delimiter_start.indexIn(text) while startIndex >= 0: endIndex = delimiter_end.indexIn(text, startIndex) commentLength = 0 if endIndex == -1: self.setCurrentBlockState(1) commentLength = text.length() - startIndex else: commentLength = endIndex - startIndex + \ delimiter_end.matchedLength() self.setFormat(startIndex, commentLength, style) startIndex = delimiter_start.indexIn(text, startIndex + commentLength)
class Highlighter(QSyntaxHighlighter): # braces braces = ['\\(', '\\)', '\\{', '\\}', '\\[', '\\]'] def __init__(self, document, lang=None, scheme=None, errors=None, pep8=None): QSyntaxHighlighter.__init__(self, document) self.highlight_function = self.realtime_highlight self.errors = errors self.pep8 = pep8 self.selected_word_lines = [] self.visible_limits = (0, 50) self._styles = {} if lang is not None: self.apply_highlight(lang, scheme) def sanitize(self, word): return word.replace('\\', '\\\\') def apply_highlight(self, lang, scheme=None, syntax=None): if syntax is None: langSyntax = settings.SYNTAX.get(lang, {}) else: langSyntax = syntax if scheme is not None: restyle(scheme) keywords = langSyntax.get('keywords', []) operators = langSyntax.get('operators', []) extras = langSyntax.get('extras', []) rules = [] # Keyword, operator, brace and extras rules keyword_pattern = '(^|[^\w\.]{1})(%s)([^\w]{1}|$)' rules += [(keyword_pattern % w, 2, STYLES['keyword']) for w in keywords] rules += [(r'%s' % o, 0, STYLES['operator']) for o in operators] rules += [(r'%s' % b, 0, STYLES['brace']) for b in Highlighter.braces] rules += [(keyword_pattern % e, 2, STYLES['extras']) for e in extras] # All other rules proper = langSyntax.get('properObject', None) if proper is not None: proper = r'\b%s\b' % str(proper[0]) rules += [(proper, 0, STYLES['properObject'])] rules.append((r'__\w+__', 0, STYLES['properObject'])) definition = langSyntax.get('definition', []) for de in definition: expr = r'\b%s\b\s*(\w+)' % de rules.append((expr, 1, STYLES['definition'])) # Numeric literals rules += [ (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), ] regex = langSyntax.get('regex', []) for reg in regex: expr = reg[0] color = resources.COLOR_SCHEME['extras'] style = '' if len(reg) > 1: if reg[1] in resources.CUSTOM_SCHEME: color = resources.CUSTOM_SCHEME[reg[1]] elif reg[1] in resources.COLOR_SCHEME: color = resources.COLOR_SCHEME[reg[1]] if len(reg) > 2: style = reg[2] rules.append((expr, 0, format(color, style))) stringChar = langSyntax.get('string', []) for sc in stringChar: expr = r'"[^"\\]*(\\.[^"\\]*)*"' if sc == '"' \ else r"'[^'\\]*(\\.[^'\\]*)*'" rules.append((expr, 0, STYLES['string'])) comments = langSyntax.get('comment', []) for co in comments: expr = co + '[^\\n]*' rules.append((expr, 0, STYLES['comment'])) # Multi-line strings (expression, flag, style) # FIXME: The triple-quotes in these two lines will mess up the # syntax highlighting from this point onward self.tri_single = (QRegExp("'''"), 1, STYLES["string2"]) self.tri_double = (QRegExp('"""'), 2, STYLES['string2']) multi = langSyntax.get('multiline_comment', []) if multi: self.multi_start = (QRegExp(multi['open']), STYLES['comment']) self.multi_end = (QRegExp(multi['close']), STYLES['comment']) else: self.multi_start = None # Build a QRegExp for each pattern self.rules = [(QRegExp(pat), index, fmt) for (pat, index, fmt) in rules] self.selected_word_pattern = None #Apply Highlight to the document... (when colors change) self.rehighlight() def set_selected_word(self, word): """Set the word to highlight.""" if len(word) > 2: self.selected_word_pattern = QRegExp( r'\b%s\b' % self.sanitize(word)) else: self.selected_word_pattern = None def __highlight_pep8(self, char_format, user_data): """Highlight the lines with errors.""" user_data.error = True char_format = char_format.toCharFormat() char_format.setUnderlineColor(QColor( resources.CUSTOM_SCHEME.get('pep8-underline', resources.COLOR_SCHEME['pep8-underline']))) char_format.setUnderlineStyle( QTextCharFormat.WaveUnderline) return char_format def __highlight_lint(self, char_format, user_data): """Highlight the lines with errors.""" user_data.error = True char_format = char_format.toCharFormat() char_format.setUnderlineColor(QColor( resources.CUSTOM_SCHEME.get('error-underline', resources.COLOR_SCHEME['error-underline']))) char_format.setUnderlineStyle( QTextCharFormat.WaveUnderline) return char_format def highlightBlock(self, text): """Apply syntax highlighting to the given block of text.""" self.highlight_function(text) def set_open_visible_area(self, is_line, position): if is_line: self.visible_limits = (position - 50, position + 50) def open_highlight(self, text): if self.visible_limits[0] <= self.currentBlock().blockNumber() <= \ self.visible_limits[1]: self.realtime_highlight(text) else: self.setCurrentBlockState(0) def async_highlight(self): self.thread_highlight = HighlightParserThread(self) self.connect(self.thread_highlight, SIGNAL("highlightingDetected(PyQt_PyObject)"), self._execute_threaded_highlight) self.thread_highlight.start() def _execute_threaded_highlight(self, styles=None): self.highlight_function = self.threaded_highlight if styles: self._styles = styles lines = list(set(styles.keys()) - set(range(self.visible_limits[0], self.visible_limits[1]))) self.rehighlight_lines(lines, False) else: self._styles = {} self.highlight_function = self.realtime_highlight def threaded_highlight(self, text): hls = [] block = self.currentBlock() user_data = block.userData() if user_data is None: user_data = SyntaxUserData(False) user_data.clear_data() block_number = block.blockNumber() highlight_errors = lambda cf, ud: cf if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 char_format = block.charFormat() char_format = highlight_errors(char_format, user_data) self.setFormat(0, len(block.text()), char_format) block_styles = self._styles.get(block.blockNumber(), ()) for index, length, char_format in block_styles: char_format = highlight_errors(char_format, user_data) if (self.format(index) != STYLES['string']): self.setFormat(index, length, char_format) if char_format == STYLES['string']: hls.append((index, index + length)) user_data.add_str_group(index, index + length) elif char_format == STYLES['comment']: user_data.comment_start_at(index) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline(text, *self.tri_single, hls=hls, highlight_errors=highlight_errors, user_data=user_data) if not in_multiline: in_multiline = self.match_multiline(text, *self.tri_double, hls=hls, highlight_errors=highlight_errors, user_data=user_data) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) block.setUserData(user_data) def realtime_highlight(self, text): hls = [] block = self.currentBlock() user_data = block.userData() if user_data is None: user_data = SyntaxUserData(False) user_data.clear_data() block_number = block.blockNumber() highlight_errors = lambda cf, ud: cf if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 char_format = block.charFormat() char_format = highlight_errors(char_format, user_data) self.setFormat(0, len(block.text()), char_format) for expression, nth, char_format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = expression.cap(nth).length() char_format = highlight_errors(char_format, user_data) if (self.format(index) != STYLES['string']): self.setFormat(index, length, char_format) if char_format == STYLES['string']: hls.append((index, index + length)) user_data.add_str_group(index, index + length) elif char_format == STYLES['comment']: user_data.comment_start_at(index) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline(text, *self.tri_single, hls=hls, highlight_errors=highlight_errors, user_data=user_data) if not in_multiline: in_multiline = self.match_multiline(text, *self.tri_double, hls=hls, highlight_errors=highlight_errors, user_data=user_data) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) #Highlight selected word if self.selected_word_pattern is not None: index = self.selected_word_pattern.indexIn(text, 0) while index >= 0: index = self.selected_word_pattern.pos(0) length = self.selected_word_pattern.cap(0).length() char_format = self.format(index) color = QColor() color.setNamedColor(STYLES['selectedWord']) color.setAlpha(100) char_format.setBackground(color) self.setFormat(index, length, char_format) index = self.selected_word_pattern.indexIn( text, index + length) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = expression.cap(0).length() char_format = STYLES['spaces'] if settings.HIGHLIGHT_WHOLE_LINE: char_format = highlight_errors(char_format, user_data) self.setFormat(index, length, char_format) index = expression.indexIn(text, index + length) block.setUserData(user_data) def _rehighlight_lines(self, lines): if self.document() is None: return for line in lines: block = self.document().findBlockByNumber(line) self.rehighlightBlock(block) def _get_errors_lines(self): errors_lines = [] block = self.document().begin() while block.isValid(): user_data = block.userData() if (user_data is not None) and (user_data.error == True): errors_lines.append(block.blockNumber()) block = block.next() return errors_lines def rehighlight_lines(self, lines, errors=True): if errors: errors_lines = self._get_errors_lines() refresh_lines = set(lines + errors_lines) else: refresh_lines = set(lines + self.selected_word_lines) self.selected_word_lines = lines self._rehighlight_lines(refresh_lines) def match_multiline(self, text, delimiter, in_state, style, hls=[], highlight_errors=lambda x: x, user_data=None): """Do highlighting of multi-line strings. ``delimiter`` should be a ``QRegExp`` for triple-single-quotes or triple-double-quotes, and ``in_state`` should be a unique integer to represent the corresponding state changes when inside those strings. Returns True if we're still inside a multi-line string when this function is finished. """ # If inside triple-single quotes, start at 0 if self.previousBlockState() == in_state: start = 0 add = 0 # Otherwise, look for the delimiter on this line else: start = delimiter.indexIn(text) # Move past this match add = delimiter.matchedLength() # As long as there's a delimiter match on this line... while start >= 0: # Look for the ending delimiter end = delimiter.indexIn(text, start + add) # Ending delimiter on this line? if end >= add: length = end - start + add + delimiter.matchedLength() self.setCurrentBlockState(0) # No; multi-line string else: self.setCurrentBlockState(in_state) length = text.length() - start + add st_fmt = self.format(start) start_collides = [pos for pos in hls if pos[0] < start < pos[1]] # Apply formatting if ((st_fmt != STYLES['comment']) or \ ((st_fmt == STYLES['comment']) and (self.previousBlockState() != 0))) and \ (len(start_collides) == 0): if user_data is not None: style = highlight_errors(style, user_data) self.setFormat(start, length, style) else: self.setCurrentBlockState(0) # Look for the next match start = delimiter.indexIn(text, start + length) # Return True if still inside a multi-line string, False otherwise if self.currentBlockState() == in_state: return True else: return False def comment_multiline(self, text, delimiter_end, delimiter_start, style): startIndex = 0 if self.previousBlockState() != 1: startIndex = delimiter_start.indexIn(text) while startIndex >= 0: endIndex = delimiter_end.indexIn(text, startIndex) commentLength = 0 if endIndex == -1: self.setCurrentBlockState(1) commentLength = text.length() - startIndex else: commentLength = endIndex - startIndex + \ delimiter_end.matchedLength() self.setFormat(startIndex, commentLength, style) startIndex = delimiter_start.indexIn(text, startIndex + commentLength)
def realtime_highlight(self, text): hls = [] block = self.currentBlock() block_number = block.blockNumber() highlight_errors = self.__clean_error if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 char_format = block.charFormat() char_format = highlight_errors(char_format, block) self.setFormat(0, len(block.text()), char_format) for expression, nth, char_format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = expression.cap(nth).length() char_format = highlight_errors(char_format, block) if (self.format(index) != STYLES['string']): self.setFormat(index, length, char_format) if char_format == STYLES['string']: hls.append((index, index + length)) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline(text, *self.tri_single, hls=hls, highlight_errors=highlight_errors) if not in_multiline: in_multiline = self.match_multiline(text, *self.tri_double, hls=hls, highlight_errors=highlight_errors) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) #Highlight selected word if self.selected_word_pattern is not None: index = self.selected_word_pattern.indexIn(text, 0) while index >= 0: index = self.selected_word_pattern.pos(0) length = self.selected_word_pattern.cap(0).length() char_format = self.format(index) color = QColor() color.setNamedColor(STYLES['selectedWord']) color.setAlpha(100) char_format.setBackground(color) self.setFormat(index, length, char_format) index = self.selected_word_pattern.indexIn( text, index + length) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = expression.cap(0).length() char_format = STYLES['spaces'] if settings.HIGHLIGHT_WHOLE_LINE: char_format = highlight_errors(char_format, block) self.setFormat(index, length, char_format) index = expression.indexIn(text, index + length)
return try: c = self._execute(None, "SELECT srtext FROM spatial_ref_sys WHERE srid = '%d'" % srid) except DbError, e: return sr = self._fetchone(c) self._close_cursor(c) if sr is None: return srtext = sr[0] # try to extract just SR name (should be quoted in double quotes) regex = QRegExp('"([^"]+)"') if regex.indexIn(srtext) > -1: srtext = regex.cap(1) return srtext def isVectorTable(self, table): if self.has_geometry_columns and self.has_geometry_columns_access: schema, tablename = self.getSchemaTableName(table) sql = u"SELECT count(*) FROM geometry_columns WHERE f_table_schema = %s AND f_table_name = %s" % ( self.quoteString(schema), self.quoteString(tablename)) c = self._execute(None, sql) res = self._fetchone(c) self._close_cursor(c) return res is not None and res[0] > 0 return False
class SvnStatusMonitorThread(VcsStatusMonitorThread): """ Class implementing the VCS status monitor thread class for Subversion. """ def __init__(self, interval, projectDir, vcs, parent = None): """ Constructor @param interval new interval in seconds (integer) @param projectDir project directory to monitor (string or QString) @param vcs reference to the version control object @param parent reference to the parent object (QObject) """ VcsStatusMonitorThread.__init__(self, interval, projectDir, vcs, parent) self.__ioEncoding = str(Preferences.getSystem("IOEncoding")) self.rx_status1 = \ QRegExp('(.{8,9})\\s+([0-9-]+)\\s+(.+)\\s*') self.rx_status2 = \ QRegExp('(.{8,9})\\s+([0-9-]+)\\s+([0-9?]+)\\s+(\\S+)\\s+(.+)\\s*') def _performMonitor(self): """ Protected method implementing the monitoring action. This method populates the statusList member variable with a list of strings giving the status in the first column and the path relative to the project directory starting with the third column. The allowed status flags are: <ul> <li>"A" path was added but not yet comitted</li> <li>"M" path has local changes</li> <li>"O" path was removed</li> <li>"R" path was deleted and then re-added</li> <li>"U" path needs an update</li> <li>"Z" path contains a conflict</li> <li>" " path is back at normal</li> </ul> @return tuple of flag indicating successful operation (boolean) and a status message in case of non successful operation (QString) """ self.shouldUpdate = False process = QProcess() args = QStringList() args.append('status') if not Preferences.getVCS("MonitorLocalStatus"): args.append('--show-updates') args.append('--non-interactive') args.append('.') process.setWorkingDirectory(self.projectDir) process.start('svn', args) procStarted = process.waitForStarted() if procStarted: finished = process.waitForFinished(300000) if finished and process.exitCode() == 0: output = \ unicode(process.readAllStandardOutput(), self.__ioEncoding, 'replace') states = {} for line in output.splitlines(): if self.rx_status1.exactMatch(line): flags = str(self.rx_status1.cap(1)) path = self.rx_status1.cap(3).trimmed() elif self.rx_status2.exactMatch(line): flags = str(self.rx_status2.cap(1)) path = self.rx_status2.cap(5).trimmed() else: continue if flags[0] in "ACDMR" or \ (flags[0] == " " and flags[-1] == "*"): if flags[-1] == "*": status = "U" else: status = flags[0] if status == "C": status = "Z" # give it highest priority elif status == "D": status = "O" if status == "U": self.shouldUpdate = True name = unicode(path) states[name] = status try: if self.reportedStates[name] != status: self.statusList.append("%s %s" % (status, name)) except KeyError: self.statusList.append("%s %s" % (status, name)) for name in self.reportedStates.keys(): if name not in states: self.statusList.append(" %s" % name) self.reportedStates = states return True, \ self.trUtf8("Subversion status checked successfully (using svn)") else: process.kill() process.waitForFinished() return False, QString(process.readAllStandardError()) else: process.kill() process.waitForFinished() return False, self.trUtf8("Could not start the Subversion process.")
def highlightBlock(self, text): """Apply syntax highlighting to the given block of text.""" hls = [] block = self.currentBlock() block_number = self.currentBlock().blockNumber() highlight_errors = lambda x: x if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 format = block.charFormat() format = highlight_errors(format) self.setFormat(0, len(block.text()), format) for expression, nth, format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = expression.cap(nth).length() format = highlight_errors(format) if (self.format(index) != STYLES['string']): self.setFormat(index, length, format) if format == STYLES['string']: hls.append((index, index + length)) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline(text, *self.tri_single, hls=hls) if not in_multiline: in_multiline = self.match_multiline(text, *self.tri_double, hls=hls) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) #Highlight selected word if self.selected_word_pattern is not None: index = self.selected_word_pattern.indexIn(text, 0) while index >= 0: index = self.selected_word_pattern.pos(0) length = self.selected_word_pattern.cap(0).length() format = self.format(index) color = QColor() color.setNamedColor(STYLES['selectedWord']) color.setAlpha(100) format.setBackground(color) self.setFormat(index, length, format) index = self.selected_word_pattern.indexIn( text, index + length) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = expression.cap(0).length() format = STYLES['spaces'] format = highlight_errors(format) self.setFormat(index, length, format) index = expression.indexIn(text, index + length)
def loadMetadata(self, metaStream): sectionRE = QRegExp( u"\{\{(" + '|'.join ( ['PAGETABLE','CHARCENSUS','WORDCENSUS','BOOKMARKS', 'NOTES','GOODWORDS','BADWORDS','CURSOR','VERSION', 'STALECENSUS','NEEDSPELLCHECK','ENCODING', 'DOCHASH', 'MAINDICT'] ) \ + u")(.*)\}\}", Qt.CaseSensitive) metaVersion = 0 # base version while not metaStream.atEnd(): qline = metaStream.readLine().trimmed() if qline.isEmpty(): continue # allow blank lines between sections if sectionRE.exactMatch(qline): # section start section = sectionRE.cap(1) argument = unicode(sectionRE.cap(2).trimmed()) endsec = QString(u"{{/" + section + u"}}") if section == u"VERSION": if len(argument) != 0: metaVersion = int(argument) continue # no more data after {{VERSION x }} elif section == u"STALECENSUS": if argument == u"TRUE": IMC.staleCensus = IMC.staleCensusLoaded continue # no more data after {{STALECENSUS x}} elif section == u"NEEDSPELLCHECK": if argument == u"TRUE": IMC.needSpellCheck = True continue # no more data after {{NEEDSPELLCHECK x}} elif section == u"ENCODING": IMC.bookSaveEncoding = QString(argument) continue elif section == u"MAINDICT": IMC.bookMainDict = QString(argument) continue elif section == u"DOCHASH": IMC.metaHash = argument continue elif section == u"PAGETABLE": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and ( not qline.isEmpty()): IMC.pageTable.metaStringIn(qline) qline = metaStream.readLine() continue elif section == u"CHARCENSUS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and ( not qline.isEmpty()): # can't just .split the char census, the first # char is the char being counted and it can be a space. str = unicode(qline) parts = str[2:].split(' ') IMC.charCensus.append(QString(str[0]), int(parts[0]), int(parts[1])) qline = metaStream.readLine() continue elif section == u"WORDCENSUS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and ( not qline.isEmpty()): parts = unicode(qline).split(' ') IMC.wordCensus.append(QString(parts[0]), int(parts[1]), int(parts[2])) qline = metaStream.readLine() continue elif section == u"BOOKMARKS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and ( not qline.isEmpty()): parts = unicode(qline).split(' ') tc = QTextCursor(self.document()) tc.setPosition(int(parts[1])) if len(parts ) == 3: # early versions didn't save anchor tc.movePosition(int(parts[2]), QTextCursor.KeepAnchor) self.bookMarkList[int(parts[0])] = tc qline = metaStream.readLine() continue elif section == u"NOTES": e = IMC.notesEditor e.setUndoRedoEnabled(False) qline = metaStream.readLine() while (not qline.startsWith(endsec) ) and not metaStream.atEnd(): if qline.startsWith(u"\xfffd"): # escaped {{ qline.remove(0, 1) e.appendPlainText(qline) qline = metaStream.readLine() e.setUndoRedoEnabled(True) continue elif section == u"GOODWORDS": # not going to bother checking for endsec return, # if it isn't that then we will shortly fail anyway w = IMC.goodWordList.load(metaStream, endsec) continue elif section == u"BADWORDS": w = IMC.badWordList.load(metaStream, endsec) continue elif section == u"CURSOR": # restore selection as of save p1p2 = argument.split(' ') tc = QTextCursor(self.document()) tc.setPosition(int(p1p2[0]), QTextCursor.MoveAnchor) tc.setPosition(int(p1p2[1]), QTextCursor.KeepAnchor) self.setTextCursor(tc) else: # this can't happen; section is text captured by the RE # and we have accounted for all possibilities raise AssertionError, "impossible metadata" else: # Non-blank line that doesn't match sectionRE? pqMsgs.infoMsg( "Unexpected line in metadata: {0}".format( pqMsgs.trunc(qline, 20)), "Metadata may be incomplete, suggest quit") break
class SvnChangeListsDialog(QDialog, Ui_SvnChangeListsDialog): """ Class implementing a dialog to browse the change lists. """ def __init__(self, vcs, parent=None): """ Constructor @param vcs reference to the vcs object @param parent parent widget (QWidget) """ QDialog.__init__(self, parent) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.process = None self.vcs = vcs self.rx_status = \ QRegExp('(.{8,9})\\s+([0-9-]+)\\s+([0-9?]+)\\s+(\\S+)\\s+(.+)\\s*') # flags (8 or 9 anything), revision, changed rev, author, path self.rx_status2 = \ QRegExp('(.{8,9})\\s+(.+)\\s*') # flags (8 or 9 anything), path self.rx_changelist = \ QRegExp('--- \\S+ .([\\w\\s]+).:\\s+') # three dashes, Changelist (translated), quote, # changelist name, quote, : @pyqtSignature("QListWidgetItem*, QListWidgetItem*") def on_changeLists_currentItemChanged(self, current, previous): """ Private slot to handle the selection of a new item. @param current current item (QListWidgetItem) @param previous previous current item (QListWidgetItem) """ self.filesList.clear() if current is not None: changelist = unicode(current.text()) if changelist in self.changeListsDict: self.filesList.addItems(sorted(self.changeListsDict[changelist])) def start(self, path): """ Public slot to populate the data. @param path directory name to show change lists for (string) """ self.changeListsDict = {} self.filesLabel.setText(self.trUtf8("Files (relative to %1):").arg(path)) self.errorGroup.hide() self.intercept = False self.path = path self.currentChangelist = "" self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) args = [] args.append('status') self.vcs.addArguments(args, self.vcs.options['global']) self.vcs.addArguments(args, self.vcs.options['status']) if '--verbose' not in self.vcs.options['global'] and \ '--verbose' not in self.vcs.options['status']: args.append('--verbose') if isinstance(path, list): self.dname, fnames = self.vcs.splitPathList(path) self.vcs.addArguments(args, fnames) else: self.dname, fname = self.vcs.splitPath(path) args.append(fname) self.process.setWorkingDirectory(self.dname) self.process.start('svn', args) procStarted = self.process.waitForStarted() if not procStarted: self.inputGroup.setEnabled(False) self.inputGroup.hide() KQMessageBox.critical(self, self.trUtf8('Process Generation Error'), self.trUtf8( 'The process %1 could not be started. ' 'Ensure, that it is in the search path.' ).arg('svn')) else: self.inputGroup.setEnabled(True) self.inputGroup.show() def __finish(self): """ Private slot called when the process finished or the user pressed the button. """ if self.process is not None and \ self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.inputGroup.setEnabled(False) self.inputGroup.hide() if len(self.changeListsDict) == 0: self.changeLists.addItem(self.trUtf8("No changelists found")) self.buttonBox.button(QDialogButtonBox.Close).setFocus(Qt.OtherFocusReason) else: self.changeLists.addItems(sorted(self.changeListsDict.keys())) self.changeLists.setCurrentRow(0) self.changeLists.setFocus(Qt.OtherFocusReason) def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): self.__finish() def __procFinished(self, exitCode, exitStatus): """ Private slot connected to the finished signal. @param exitCode exit code of the process (integer) @param exitStatus exit status of the process (QProcess.ExitStatus) """ self.__finish() def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process, formats it and inserts it into the contents pane. """ if self.process is not None: self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = unicode(self.process.readLine(), Preferences.getSystem("IOEncoding"), 'replace') if self.currentChangelist != "" and self.rx_status.exactMatch(s): file = unicode(self.rx_status.cap(5)).strip() filename = file.replace(self.path + os.sep, "") if filename not in self.changeListsDict[self.currentChangelist]: self.changeListsDict[self.currentChangelist].append(filename) elif self.currentChangelist != "" and self.rx_status2.exactMatch(s): file = unicode(self.rx_status2.cap(2)).strip() filename = file.replace(self.path + os.sep, "") if filename not in self.changeListsDict[self.currentChangelist]: self.changeListsDict[self.currentChangelist].append(filename) elif self.rx_changelist.exactMatch(s): self.currentChangelist = unicode(self.rx_changelist.cap(1)) if self.currentChangelist not in self.changeListsDict: self.changeListsDict[self.currentChangelist] = [] def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ if self.process is not None: self.errorGroup.show() s = QString(self.process.readAllStandardError()) self.errors.insertPlainText(s) self.errors.ensureCursorVisible() def on_passwordCheckBox_toggled(self, isOn): """ Private slot to handle the password checkbox toggled. @param isOn flag indicating the status of the check box (boolean) """ if isOn: self.input.setEchoMode(QLineEdit.Password) else: self.input.setEchoMode(QLineEdit.Normal) @pyqtSignature("") def on_sendButton_clicked(self): """ Private slot to send the input to the subversion process. """ input = self.input.text() input += os.linesep if self.passwordCheckBox.isChecked(): self.errors.insertPlainText(os.linesep) self.errors.ensureCursorVisible() else: self.errors.insertPlainText(input) self.errors.ensureCursorVisible() self.process.write(input) self.passwordCheckBox.setChecked(False) self.input.clear() def on_input_returnPressed(self): """ Private slot to handle the press of the return key in the input field. """ self.intercept = True self.on_sendButton_clicked() def keyPressEvent(self, evt): """ Protected slot to handle a key press event. @param evt the key press event (QKeyEvent) """ if self.intercept: self.intercept = False evt.accept() return QDialog.keyPressEvent(self, evt)
return try: c = self._execute(None, "SELECT srtext FROM spatial_ref_sys WHERE srid = '%d'" % srid) except DbError, e: return sr = self._fetchone(c) self._close_cursor(c) if sr is None: return srtext = sr[0] # try to extract just SR name (should be quoted in double quotes) regex = QRegExp('"([^"]+)"') if regex.indexIn(srtext) > -1: srtext = regex.cap(1) return srtext def isVectorTable(self, table): if self.has_geometry_columns and self.has_geometry_columns_access: schema, tablename = self.getSchemaTableName(table) sql = u"SELECT count(*) FROM geometry_columns WHERE f_table_schema = %s AND f_table_name = %s" % ( self.quoteString(schema), self.quoteString(tablename), ) c = self._execute(None, sql) res = self._fetchone(c) self._close_cursor(c) return res is not None and res[0] > 0
def loadMetadata(self,metaStream): sectionRE = QRegExp( u"\{\{(" + '|'.join ( ['PAGETABLE','CHARCENSUS','WORDCENSUS','BOOKMARKS', 'NOTES','GOODWORDS','BADWORDS','CURSOR','VERSION', 'STALECENSUS','NEEDSPELLCHECK','ENCODING', 'DOCHASH', 'MAINDICT'] ) \ + u")(.*)\}\}", Qt.CaseSensitive) metaVersion = 0 # base version while not metaStream.atEnd() : qline = metaStream.readLine().trimmed() if qline.isEmpty() : continue # allow blank lines between sections if sectionRE.exactMatch(qline) : # section start section = sectionRE.cap(1) argument = unicode(sectionRE.cap(2).trimmed()) endsec = QString(u"{{/" + section + u"}}") if section == u"VERSION": if len(argument) != 0 : metaVersion = int(argument) continue # no more data after {{VERSION x }} elif section == u"STALECENSUS" : if argument == u"TRUE" : IMC.staleCensus = IMC.staleCensusLoaded continue # no more data after {{STALECENSUS x}} elif section == u"NEEDSPELLCHECK" : if argument == u"TRUE" : IMC.needSpellCheck = True continue # no more data after {{NEEDSPELLCHECK x}} elif section == u"ENCODING" : IMC.bookSaveEncoding = QString(argument) continue elif section == u"MAINDICT" : IMC.bookMainDict = QString(argument) continue elif section == u"DOCHASH" : IMC.metaHash = argument continue elif section == u"PAGETABLE": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and (not qline.isEmpty()): IMC.pageTable.metaStringIn(qline) qline = metaStream.readLine() continue elif section == u"CHARCENSUS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and (not qline.isEmpty()): # can't just .split the char census, the first # char is the char being counted and it can be a space. str = unicode(qline) parts = str[2:].split(' ') IMC.charCensus.append(QString(str[0]),int(parts[0]),int(parts[1])) qline = metaStream.readLine() continue elif section == u"WORDCENSUS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and (not qline.isEmpty()): parts = unicode(qline).split(' ') IMC.wordCensus.append(QString(parts[0]),int(parts[1]),int(parts[2])) qline = metaStream.readLine() continue elif section == u"BOOKMARKS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and (not qline.isEmpty()): parts = unicode(qline).split(' ') tc = QTextCursor(self.document() ) tc.setPosition(int(parts[1])) if len(parts) == 3 : # early versions didn't save anchor tc.movePosition(int(parts[2]),QTextCursor.KeepAnchor) self.bookMarkList[int(parts[0])] = tc qline = metaStream.readLine() continue elif section == u"NOTES": e = IMC.notesEditor e.setUndoRedoEnabled(False) qline = metaStream.readLine() while (not qline.startsWith(endsec)) and not metaStream.atEnd(): if qline.startsWith(u"\xfffd"): # escaped {{ qline.remove(0,1) e.appendPlainText(qline) qline = metaStream.readLine() e.setUndoRedoEnabled(True) continue elif section == u"GOODWORDS" : # not going to bother checking for endsec return, # if it isn't that then we will shortly fail anyway w = IMC.goodWordList.load(metaStream,endsec) continue elif section == u"BADWORDS" : w = IMC.badWordList.load(metaStream,endsec) continue elif section == u"CURSOR" : # restore selection as of save p1p2 = argument.split(' ') tc = QTextCursor(self.document()) tc.setPosition(int(p1p2[0]),QTextCursor.MoveAnchor) tc.setPosition(int(p1p2[1]),QTextCursor.KeepAnchor) self.setTextCursor(tc) else: # this can't happen; section is text captured by the RE # and we have accounted for all possibilities raise AssertionError, "impossible metadata" else: # Non-blank line that doesn't match sectionRE? pqMsgs.infoMsg( "Unexpected line in metadata: {0}".format(pqMsgs.trunc(qline,20)), "Metadata may be incomplete, suggest quit") break
class PPTextEditor(QPlainTextEdit): # Initialize the editor on creation. def __init__(self, parent=None, fontsize=12): super(PPTextEditor, self).__init__(parent) # Do not allow line-wrap; horizontal scrollbar appears when required. self.setLineWrapMode(QPlainTextEdit.NoWrap) # make sure when we jump to a line, it goes to the window center self.setCenterOnScroll(True) # Get a monospaced font as selected by the user with View>Font self.setFont(pqMsgs.getMonoFont(fontsize, True)) # instantiate our "syntax" highlighter object, but link it to an empty # QTextDocument. We will redirect it to our actual document only after # loading a document, as it relies on metadata, and then only when/if # the IMC.*HiliteSwitch es are on. self.nulDoc = QTextDocument() # make a null document self.hiliter = wordHighLighter(self.nulDoc) # all the metadata lists will be initialized when self.clear() is # called from pqMain, shortly. # save a regex for quickly finding if a selection is a single word self.oneWordRE = QRegExp(u'^\W*(\w{2,})\W*$') self.menuWord = QString() # Create and initialize an SHA-1 hash machine self.cuisineart = QCryptographicHash(QCryptographicHash.Sha1) # switch on or off our text-highlighting. By switching the highlighter # to a null document we remove highlighting; by switching it back to # the real document, we cause re-highlighting of everything. This makes # significant delay for a large document, so put up a status message # during it by starting and ending a progress bar. def setHighlight(self, onoff): self.hiliter.setDocument(self.nulDoc) # turn off hiliting always if onoff: pqMsgs.showStatusMsg("Setting Scanno/Spelling Highlights...") self.hiliter.setDocument(self.document()) pqMsgs.clearStatusMsg() # Implement clear/new. Just toss everything we keep. def clear(self): self.setHighlight(False) self.document().clear() self.document().setModified(False) self.bookMarkList = \ [None, None, None, None, None, None, None, None, None] IMC.pageTable.clear() IMC.goodWordList.clear() IMC.badWordList.clear() IMC.wordCensus.clear() IMC.charCensus.clear() IMC.notesEditor.clear() IMC.pngPanel.clear() IMC.needSpellCheck = False IMC.needMetadataSave = 0x00 IMC.staleCensus = 0x00 IMC.bookSaveEncoding = QString(u'UTF-8') IMC.bookMainDict = IMC.spellCheck.mainTag # force a cursor "move" in order to create a cursorMoved signal that will # clear the status line - then undo it so the document isn't modified. self.textCursor().insertText(QString(' ')) self.document().undo() # Implement the Edit menu items: # Edit > ToUpper, Edit > ToTitle, Edit > ToLower # Note that in full Unicode, changing letter case is not so simple as it # was in Latin-1! We use the QChar and QString facilities to do it, and # a regex in a loop to pick off words. Restore the current selection after # so another operation can be done on it. # N.B. it is not possible to do self.textCursor().setPosition(), it seems # that self.textCursor() is "const". One has to create a new cursor, # position it, and install it on the document with self.setTextCursor(). def toUpperCase(self): global reWord tc = QTextCursor(self.textCursor()) if not tc.hasSelection(): return # no selection, nothing to do startpos = tc.selectionStart() endpos = tc.selectionEnd() qs = QString(tc.selectedText()) # copy of selected text i = reWord.indexIn(qs, 0) # index of first word if any if i < 0: return # no words in selection, exit while i >= 0: w = reWord.cap(0) # found word as QString n = w.size() # its length qs.replace(i, n, w.toUpper()) # replace it with UC version i = reWord.indexIn(qs, i + n) # find next word if any # we have changed at least one word, replace selection with altered text tc.insertText(qs) # that wiped the selection, so restore it by "dragging" left to right tc.setPosition(startpos, QTextCursor.MoveAnchor) # click tc.setPosition(endpos, QTextCursor.KeepAnchor) # drag self.setTextCursor(tc) # to-lower is identical except for the method call. def toLowerCase(self): global reWord # the regex \b\w+\b tc = QTextCursor(self.textCursor()) if not tc.hasSelection(): return # no selection, nothing to do startpos = tc.selectionStart() endpos = tc.selectionEnd() qs = QString(tc.selectedText()) # copy of selected text i = reWord.indexIn(qs, 0) # index of first word if any if i < 0: return # no words in selection, exit while i >= 0: w = reWord.cap(0) # found word as QString n = w.size() # its length qs.replace(i, n, w.toLower()) # replace it with UC version i = reWord.indexIn(qs, i + n) # find next word if any # we have changed at least one word, replace selection with altered text tc.insertText(qs) # that wiped the selection, so restore it by "dragging" left to right tc.setPosition(startpos, QTextCursor.MoveAnchor) # click tc.setPosition(endpos, QTextCursor.KeepAnchor) # drag self.setTextCursor(tc) # toTitle is similar but we have to change the word to lowercase (in case # it is uppercase now) and then change the initial character to upper. # Note it would be possible to write a smarter version that looked up the # word in a list of common adjectives, connectives, and adverbs and avoided # capitalizing a, and, of, by and so forth. Not gonna happen. def toTitleCase(self): global reWord # the regex \b\w+\b self.toLowerCase() tc = QTextCursor(self.textCursor()) if not tc.hasSelection(): return # no selection, nothing to do startpos = tc.selectionStart() endpos = tc.selectionEnd() qs = QString(tc.selectedText()) # copy of selected text i = reWord.indexIn(qs, 0) # index of first word if any if i < 0: return # no words in selection, exit while i >= 0: w = reWord.cap(0) # found word as QString n = w.size() qs.replace(i, 1, qs.at(i).toUpper()) # replace initial with UC i = reWord.indexIn(qs, i + n) # find next word if any # we have changed at least one word, replace selection with altered text tc.insertText(qs) # that wiped the selection, so restore it by "dragging" left to right tc.setPosition(startpos, QTextCursor.MoveAnchor) # click tc.setPosition(endpos, QTextCursor.KeepAnchor) # drag self.setTextCursor(tc) # Re-implement the parent's keyPressEvent in order to provide some # special controls. (Note on Mac, "ctrl-" is "cmd-" and "alt-" is "opt-") # ctrl-plus increases the edit font size 1 pt # (n.b. ctrl-plus probably only comes from a keypad, we usually just get # ctrl-shift-equals instead of plus) # ctrl-minus decreases the edit font size 1 pt # ctrl-<n> for n in 1..9 jumps the insertion point to bookmark <n> # ctrl-shift-<n> extends the selection to bookmark <n> # ctrl-alt-<n> sets bookmark n at the current position def keyPressEvent(self, event): #pqMsgs.printKeyEvent(event) kkey = int(int(event.modifiers()) & IMC.keypadDeModifier) | int( event.key()) # add as little overhead as possible: if it isn't ours, pass it on. if kkey in IMC.keysOfInterest: # we trust python to do this quickly event.accept() # we handle this one if kkey in IMC.findKeys: # ^f, ^g, etc. -- just pass them straight to the Find panel self.emit(SIGNAL("editKeyPress"), kkey) elif kkey in IMC.zoomKeys: # n.b. the self.font and setFont methods inherit from QWidget # Point increment by which to change. n = (-1) if (kkey == IMC.ctl_minus) else 1 # Actual point size currently in use, plus increment p = self.fontInfo().pointSize() + n if (p > 3) and (p < 65): # don't let's get ridiculous, hmm? # Simply calling self.font().setPointSize() had no effect, # we have to actually call setFont() to make change happen. f = self.font() # so get our font, f.setPointSize(p) # change its point size +/- self.setFont(f) # and put the font back IMC.fontSize = p # and remember the size for shutdown time elif kkey in IMC.markKeys: # ^1-9, jump to bookmark bkn = kkey - IMC.ctl_1 # make it 0-8 if self.bookMarkList[ bkn] is not None: # if that bookmark is set, self.setTextCursor(self.bookMarkList[bkn]) # jump to it elif kkey in IMC.markShiftKeys: # shift-ctl-1/9, select to mark # Make our document cursor's selection go from our current ANCHOR # to the POSITION from the bookmark cursor. mark_tc = self.bookMarkList[kkey - IMC.ctl_shft_1] if mark_tc is not None: tc = QTextCursor(self.textCursor()) tc.setPosition(mark_tc.position(), QTextCursor.KeepAnchor) self.setTextCursor(tc) elif kkey in IMC.markSetKeys: # ctl-alt-1-9, set a bookmark bkn = kkey - IMC.ctl_alt_1 # make it 0-8 self.bookMarkList[bkn] = QTextCursor(self.textCursor()) IMC.needMetadataSave |= IMC.bookmarksChanged else: # not in keysOfInterest, so pass it up to parent event.ignore() super(PPTextEditor, self).keyPressEvent(event) # Called from pqFind after doing a successful search, this method centers the # current selection (which is the result of the find) in the window. If the selection # is large, put the top of the selection higher than center but on no account # above the top of the viewport. Two problems arise: One, the rectangles returned # by .cursorRect() and by .viewport().geometry() are in pixel units, while the # vertical scrollbar is sized in logical text lines. So we work out the adjustment # as a fraction of the viewport, times the scrollbar's pageStep value to get lines. # Two, cursorRect gives only the height of the actual cursor, not of the selected # text. To find out the height of the full selection we have to get a cursorRect # for the start of the selection, and another for the end of it. def centerCursor(self): tc = QTextCursor( self.textCursor()) # copy the working cursor with its selection top_point = tc.position() # one end of selection, in character units bot_point = tc.anchor() # ..and the other end if top_point > bot_point: # often the position is > the anchor (top_point, bot_point) = (bot_point, top_point) tc.setPosition(top_point) # cursor for the top of the selection selection_top = self.cursorRect(tc).top() # ..get its top pixel line_height = self.cursorRect( tc).height() # and save height of one line tc.setPosition(bot_point) # cursor for the end of the selection selection_bot = self.cursorRect( tc).bottom() # ..selection's bottom pixel selection_height = selection_bot - selection_top + 1 # selection height in pixels view_height = self.viewport().geometry().height( ) # scrolled area's height in px view_half = view_height >> 1 # int(view_height/2) pixel_adjustment = 0 if selection_height < view_half: # selected text is less than half the window height: center the top of the # selection, i.e., make the cursor_top equal to view_half. pixel_adjustment = selection_top - view_half # may be negative else: # selected text is taller than half the window, can we show it all? if selection_height < (view_height - line_height): # all selected text fits in the viewport (with a little free): center it. pixel_adjustment = (selection_top + (selection_height / 2)) - view_half else: # not all selected text fits the window, put text top near window top pixel_adjustment = selection_top - line_height # OK, convert the pixel adjustment to a line-adjustment based on the assumption # that a scrollbar pageStep is the height of the viewport in lines. adjust_fraction = pixel_adjustment / view_height vscroller = self.verticalScrollBar() page_step = vscroller.pageStep( ) # lines in a viewport page, actually less 1 adjust_lines = int(page_step * adjust_fraction) target = vscroller.value() + adjust_lines if (target >= 0) and (target <= vscroller.maximum()): vscroller.setValue(target) # Catch the contextMenu event and extend the standard context menu with # a separator and the option to add a word to good-words, but only when # there is a selection and it encompasses just one word. def contextMenuEvent(self, event): ctx_menu = self.createStandardContextMenu() if self.textCursor().hasSelection: qs = self.textCursor().selectedText() if 0 == self.oneWordRE.indexIn( qs): # it matches at 0 or not at all self.menuWord = self.oneWordRE.cap(1) # save the word ctx_menu.addSeparator() gw_name = QString(self.menuWord) # make a copy gw_action = ctx_menu.addAction( gw_name.append(QString(u' -> Goodwords'))) self.connect(gw_action, SIGNAL("triggered()"), self.addToGW) ctx_menu.exec_(event.globalPos()) # This slot receives the "someword -> good_words" context menu action def addToGW(self): IMC.goodWordList.insert(self.menuWord) IMC.needMetadataSave |= IMC.goodwordsChanged IMC.needSpellCheck = True IMC.mainWindow.setWinModStatus() # Implement save: the main window opens the files for output using # QIODevice::WriteOnly, which wipes the contents (contrary to the doc) # so we need to write the document and metadata regardless of whether # they've been modified. However we avoid rebuilding metadata if we can. def save(self, dataStream, metaStream): # Get the contents of the document as a QString doc_text = self.toPlainText() # Calculate the SHA-1 hash over the document and save it in both hash # fields of the IMC. self.cuisineart.reset() self.cuisineart.addData(doc_text) IMC.metaHash = IMC.documentHash = bytes( self.cuisineart.result()).__repr__() # write the document, which is pretty simple in the QStream world dataStream << doc_text dataStream.flush() #self.rebuildMetadata() # update any census that needs it self.writeMetadata(metaStream) metaStream.flush() IMC.needMetadataSave = 0x00 self.document().setModified( False) # this triggers main.setWinModStatus() def writeMetadata(self, metaStream): # Writing the metadata takes a bit more work. # pageTable goes out between {{PAGETABLE}}..{{/PAGETABLE}} metaStream << u"{{VERSION 0}}\n" # meaningless at the moment metaStream << u"{{ENCODING " metaStream << unicode(IMC.bookSaveEncoding) metaStream << u"}}\n" metaStream << u"{{STALECENSUS " if 0 == IMC.staleCensus: metaStream << u"FALSE" else: metaStream << u"TRUE" metaStream << u"}}\n" metaStream << u"{{NEEDSPELLCHECK " if 0 == IMC.needSpellCheck: metaStream << u"FALSE" else: metaStream << u"TRUE" metaStream << u"}}\n" metaStream << u"{{MAINDICT " metaStream << unicode(IMC.bookMainDict) metaStream << u"}}\n" # The hash could contain any character. Using __repr__ ensured # it is enclosed in balanced single or double quotes but to be # double sure we will fence it in characters we can spot with a regex. metaStream << u"{{DOCHASH " + IMC.documentHash + u" }}\n" if IMC.pageTable.size(): metaStream << u"{{PAGETABLE}}\n" for i in range(IMC.pageTable.size()): metaStream << IMC.pageTable.metaStringOut(i) metaStream << u"{{/PAGETABLE}}\n" if IMC.charCensus.size(): metaStream << u"{{CHARCENSUS}}\n" for i in range(IMC.charCensus.size()): (w, n, f) = IMC.charCensus.get(i) metaStream << "{0} {1} {2}\n".format(unicode(w), n, f) metaStream << u"{{/CHARCENSUS}}\n" if IMC.wordCensus.size(): metaStream << u"{{WORDCENSUS}}\n" for i in range(IMC.wordCensus.size()): (w, n, f) = IMC.wordCensus.get(i) metaStream << "{0} {1} {2}\n".format(unicode(w), n, f) metaStream << u"{{/WORDCENSUS}}\n" metaStream << u"{{BOOKMARKS}}\n" for i in range(9): # 0..8 if self.bookMarkList[i] is not None: metaStream << "{0} {1} {2}\n".format( i, self.bookMarkList[i].position(), self.bookMarkList[i].anchor()) metaStream << u"{{/BOOKMARKS}}\n" metaStream << u"{{NOTES}}\n" d = IMC.notesEditor.document() if not d.isEmpty(): for i in range(d.blockCount()): t = d.findBlockByNumber(i).text() if t.startsWith("{{"): t.prepend(u"\xfffd") # Unicode Replacement char metaStream << t + "\n" IMC.notesEditor.document().setModified(False) metaStream << u"{{/NOTES}}\n" if IMC.goodWordList.active(): # have some good words metaStream << u"{{GOODWORDS}}\n" IMC.goodWordList.save(metaStream) metaStream << u"{{/GOODWORDS}}\n" if IMC.badWordList.active(): # have some bad words metaStream << u"{{BADWORDS}}\n" IMC.badWordList.save(metaStream) metaStream << u"{{/BADWORDS}}\n" p1 = self.textCursor().selectionStart() p2 = self.textCursor().selectionEnd() metaStream << u"{{CURSOR " + unicode(p1) + u' ' + unicode(p2) + u"}}\n" metaStream.flush() # Implement load: the main window has the job of finding and opening files # then passes QTextStreams ready to read here. If metaStream is None, # no metadata file was found and we construct the metadata. # n.b. before main calls here, it calls our .clear, hence lists are # empty, hiliting is off, etc. def load(self, dataStream, metaStream, goodStream, badStream): # Load the document file into the editor self.setPlainText(dataStream.readAll()) # Initialize the hash value for the document, which will be equal unless # we read something different from the metadata file. self.cuisineart.reset() self.cuisineart.addData(self.toPlainText()) IMC.metaHash = IMC.documentHash = bytes( self.cuisineart.result()).__repr__() if metaStream is None: # load goodwords, badwords, and take census if goodStream is not None: IMC.goodWordList.load(goodStream) if badStream is not None: IMC.badWordList.load(badStream) self.rebuildMetadata( page=True) # build page table & vocab from scratch else: self.loadMetadata(metaStream) # If the metaData and document hashes now disagree, it is because the metadata # had a DOCHASH value for a different book or version. Warn the user. if IMC.metaHash != IMC.documentHash: pqMsgs.warningMsg( u"The document file and metadata file do not match!", u"Bookmarks, page breaks and other metadata will be wrong! Strongly recommend you not edit or save this book." ) # restore hiliting if the user wanted it. Note this can cause a # serious delay if the new book is large. However the alternative is # to not set it on and then we are out of step with the View menu # toggles, so the user has to set it off before loading, or suffer. self.setHighlight(IMC.scannoHiliteSwitch or IMC.spellingHiliteSwitch) # set a different main dict if there was one in the metadata if IMC.bookMainDict is not None: IMC.spellCheck.setMainDict(IMC.bookMainDict) # load page table & vocab from the .meta file as a stream. # n.b. QString has a split method we could use but instead # we take the input line to a Python u-string and split it. For # the word/char census we have to take the key back to a QString. def loadMetadata(self, metaStream): sectionRE = QRegExp( u"\{\{(" + '|'.join ( ['PAGETABLE','CHARCENSUS','WORDCENSUS','BOOKMARKS', 'NOTES','GOODWORDS','BADWORDS','CURSOR','VERSION', 'STALECENSUS','NEEDSPELLCHECK','ENCODING', 'DOCHASH', 'MAINDICT'] ) \ + u")(.*)\}\}", Qt.CaseSensitive) metaVersion = 0 # base version while not metaStream.atEnd(): qline = metaStream.readLine().trimmed() if qline.isEmpty(): continue # allow blank lines between sections if sectionRE.exactMatch(qline): # section start section = sectionRE.cap(1) argument = unicode(sectionRE.cap(2).trimmed()) endsec = QString(u"{{/" + section + u"}}") if section == u"VERSION": if len(argument) != 0: metaVersion = int(argument) continue # no more data after {{VERSION x }} elif section == u"STALECENSUS": if argument == u"TRUE": IMC.staleCensus = IMC.staleCensusLoaded continue # no more data after {{STALECENSUS x}} elif section == u"NEEDSPELLCHECK": if argument == u"TRUE": IMC.needSpellCheck = True continue # no more data after {{NEEDSPELLCHECK x}} elif section == u"ENCODING": IMC.bookSaveEncoding = QString(argument) continue elif section == u"MAINDICT": IMC.bookMainDict = QString(argument) continue elif section == u"DOCHASH": IMC.metaHash = argument continue elif section == u"PAGETABLE": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and ( not qline.isEmpty()): IMC.pageTable.metaStringIn(qline) qline = metaStream.readLine() continue elif section == u"CHARCENSUS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and ( not qline.isEmpty()): # can't just .split the char census, the first # char is the char being counted and it can be a space. str = unicode(qline) parts = str[2:].split(' ') IMC.charCensus.append(QString(str[0]), int(parts[0]), int(parts[1])) qline = metaStream.readLine() continue elif section == u"WORDCENSUS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and ( not qline.isEmpty()): parts = unicode(qline).split(' ') IMC.wordCensus.append(QString(parts[0]), int(parts[1]), int(parts[2])) qline = metaStream.readLine() continue elif section == u"BOOKMARKS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and ( not qline.isEmpty()): parts = unicode(qline).split(' ') tc = QTextCursor(self.document()) tc.setPosition(int(parts[1])) if len(parts ) == 3: # early versions didn't save anchor tc.movePosition(int(parts[2]), QTextCursor.KeepAnchor) self.bookMarkList[int(parts[0])] = tc qline = metaStream.readLine() continue elif section == u"NOTES": e = IMC.notesEditor e.setUndoRedoEnabled(False) qline = metaStream.readLine() while (not qline.startsWith(endsec) ) and not metaStream.atEnd(): if qline.startsWith(u"\xfffd"): # escaped {{ qline.remove(0, 1) e.appendPlainText(qline) qline = metaStream.readLine() e.setUndoRedoEnabled(True) continue elif section == u"GOODWORDS": # not going to bother checking for endsec return, # if it isn't that then we will shortly fail anyway w = IMC.goodWordList.load(metaStream, endsec) continue elif section == u"BADWORDS": w = IMC.badWordList.load(metaStream, endsec) continue elif section == u"CURSOR": # restore selection as of save p1p2 = argument.split(' ') tc = QTextCursor(self.document()) tc.setPosition(int(p1p2[0]), QTextCursor.MoveAnchor) tc.setPosition(int(p1p2[1]), QTextCursor.KeepAnchor) self.setTextCursor(tc) else: # this can't happen; section is text captured by the RE # and we have accounted for all possibilities raise AssertionError, "impossible metadata" else: # Non-blank line that doesn't match sectionRE? pqMsgs.infoMsg( "Unexpected line in metadata: {0}".format( pqMsgs.trunc(qline, 20)), "Metadata may be incomplete, suggest quit") break # Rebuild as much of the char/word census and spellcheck as we need to. # This is called from load, above, and from the Char and Word panels # Refresh buttons. If page=True we are loading a doc for which there is # no metadata file, so cache page definitions; otherwise just skip the # page definitions (see doCensus). If the doc has changed we need to # rerun the full char/word census. But if not, we might still need a # spellcheck, if the dictionary has changed. def rebuildMetadata(self, page=False): if page or (0 != IMC.staleCensus): self.doCensus(page) if IMC.needSpellCheck: self.doSpellcheck() # Go through vocabulary census and check the spelling (it would be a big # waste of time to check every word as it was read). If the spellcheck # is not up (i.e. it couldn't find a dictionary) we only mark as bad the # words in the badwords list. def doSpellcheck(self): canspell = IMC.spellCheck.isUp() nwords = IMC.wordCensus.size() if 0 >= nwords: # could be zero in a null document return pqMsgs.startBar(nwords, "Checking spelling...") for i in range(IMC.wordCensus.size()): (qword, cnt, wflags) = IMC.wordCensus.get(i) wflags = wflags & (0xff - IMC.WordMisspelt) # turn off flag if on # some words have /dict-tag, split that out as string or "" (w, x, d) = unicode(qword).partition("/") if IMC.goodWordList.check(w): pass elif IMC.badWordList.check(w): wflags |= IMC.WordMisspelt elif canspell: # check word in its optional dictionary if not (IMC.spellCheck.check(w, d)): wflags |= IMC.WordMisspelt IMC.wordCensus.setflags(i, wflags) if 0 == i & 0x1f: pqMsgs.rollBar(i) pqMsgs.endBar() IMC.needMetadataSave |= IMC.wordlistsChanged IMC.needSpellCheck = False if IMC.spellingHiliteSwitch: self.setHighlight(True) # force refresh of spell underlines # Scan the successive lines of the document and build the census of chars, # words, and (first time only) the table of page separators. # # If this is an HTML file (from IMC.bookType), and if its first line is # <!DOCTYPE..., we skip until we see <body>. This avoids polluting our # char and word censii with CSS comments and etc. Regular HTML tags # like <table> and <b> are skipped over automatically during parsing. # # Qt obligingly supplies each line as a QTextBlock. We examine the line # to see if it is a page separator. If we are opening a file having no # metadata, the Page argument is True and we build a page table entry. # Other times (e.g. from the Refresh button of the Word or Char panel), # we skip over page separator lines. # Each non-separator line is first scanned by characters and then for words. # The character scan counts characters for the Chars panel. We do NOT parse # the text for PGDP productions [oe] and [OE] nor other markups for accented # characters such as [=o] for o-with-macron or [^a] for a-with-circumflex. # These are just counted as [, o, e, ]. Reasons: (1) the alternative, to parse # them into their proper unicode values and count those, entails a whole lotta # code that would slow this census badly; (2) having the unicode chars in # the Chars panel would be confusing when they are not actually in the text; # (3) there is some value in having the counts of [ and ]. For similar reasons # we count all the chars in HTML e.g. "<i>" is three characters even though it # is effectively unprinted metadata. # In scanning words, we collect numbers as words. We collect internal hyphens # as letters ("mother-in-law") but not at end of word ("help----" or emdash). # We collect internal apostrophes ("it's", "hadn't") but not apostrophes at ends, # "'Twas" is counted as "Twas", "students' work" as "students work". This is because # there seems to be no way to distinguish the contractive prefix ('Twas) # and the final possessive (students') from normal single-quote marks! # And we collect leading and internal, but not trailing, square brackets as # letters. Thus [OE]dipus and ma[~n]ana are words (but will fail spellcheck) # while Einstein[A] (a footnote key) is not. # We also collect HTML productions ("</i>" and "<table>") as words. They do not # go in the census but we check them for lang= attributes and set the alternate # spellcheck dictionary from them. def doCensus(self, page=False): global reLineSep, reTokens, reLang, qcLess # Clear the current census values IMC.wordCensus.clear() IMC.charCensus.clear() # Count chars locally for speed local_char_census = defaultdict(int) # Name of current alternate dictionary alt_dict = QString() # isEmpty when none # Tag from which we set an alternate dict alt_dict_tag = QString() # Start the progress bar based on the number of lines in the document pqMsgs.startBar(self.document().blockCount(), "Counting words and chars...") # Find the first text block of interest, skipping an HTML header file qtb = self.document().begin() # first text block if IMC.bookType.startsWith(QString(u"htm")) \ and qtb.text().startsWith(QString(u"<!DOCTYPE")) : while (qtb != self.document().end()) \ and (not qtb.text().startsWith(QString(u"<body"))) : qtb = qtb.next() # Scan all lines of the document to the end. while qtb != self.document().end(): qsLine = qtb.text() # text of line as qstring dbg = qsLine.size() dbg2 = qtb.length() if reLineSep.exactMatch(qsLine): # this is a page separator line if page: # We are doing page seps, it's for Open with no .meta seen, # the page table has been cleared. Store the page sep # data in the page table, with a textCursor to its start. qsfilenum = reLineSep.cap(1) # xxx from "File: xxx.png" qsproofers = reLineSep.cap(2) # \who\x\blah\etc # proofer names can contain spaces, replace with en-space char qsproofers.replace(QChar(" "), QChar(0x2002)) # create a new TextCursor instance tcursor = QTextCursor(self.document()) # point it to this text block tcursor.setPosition(qtb.position()) # dump all that in the page table IMC.pageTable.loadPsep(tcursor, qsfilenum, qsproofers) # else not doing pages, just ignore this psep line else: # not psep, ordinary text line, count chars and words pyLine = unicode(qsLine) # move into Python space to count for c in pyLine: local_char_census[c] += 1 j = 0 while True: j = reTokens.indexIn(qsLine, j) if j < 0: # no more word-like units break qsWord = reTokens.cap(0) j += qsWord.size() if qsWord.startsWith(qcLess): # Examine a captured HTML production. if not reTokens.cap(2).isEmpty(): # HTML open tag, look for lang='dict' if 0 <= reLang.indexIn(reTokens.cap(3)): # found it: save tag and dict name alt_dict_tag = QString(reTokens.cap(2)) alt_dict = QString(reLang.cap(1)) alt_dict.prepend(u'/') # make "/en_GB" # else no lang= attribute else: # HTML close tag, see if it closes alt dict use if reTokens.cap(5) == alt_dict_tag: # yes, matches open-tag for dict, clear it alt_dict_tag = QString() alt_dict = QString() # else no alt dict in use, or didn't match else: # did not start with "<", process as a word # Set the property flags, which is harder now we don't # look at every character. Use the QString facilities # rather than python because python .isalnum fails # for a hyphenated number "1850-1910". flag = 0 if 0 != qsWord.compare(qsWord.toLower()): flag |= IMC.WordHasUpper if 0 != qsWord.compare(qsWord.toUpper()): flag |= IMC.WordHasLower if qsWord.contains(qcHyphen): flag |= IMC.WordHasHyphen if qsWord.contains(qcApostrophe) or qsWord.contains( qcCurlyApostrophe): flag |= IMC.WordHasApostrophe if qsWord.contains(reDigit): flag |= IMC.WordHasDigit IMC.wordCensus.count(qsWord.append(alt_dict), flag) # end "while any more words in this line" # end of not-a-psep-line processing qtb = qtb.next() # move on to next block if (0 == (qtb.blockNumber() & 255)): #every 256th block pqMsgs.rollBar(qtb.blockNumber()) # roll the bar QApplication.processEvents() # end of scanning all text blocks in the doc pqMsgs.endBar() # we accumulated the char counts in localCharCensus. Now read it out # in sorted order and stick it in the IMC.charCensus list. for one_char in sorted(local_char_census.keys()): qc = QChar(ord(one_char)) # get to QChar for category() method IMC.charCensus.append(QString(qc), local_char_census[one_char], qc.category()) IMC.needSpellCheck = True # after a census this is true IMC.staleCensus = 0 # but this is no longer true IMC.needMetadataSave |= IMC.wordlistsChanged
def realtime_highlight(self, text): """Highlight each line while it is being edited. This function apply the proper highlight to the line being edited by the user, this is a really fast process for each line once you already have the document highlighted, but slow to do it the first time to highlight all the lines together.""" hls = [] block = self.currentBlock() user_data = syntax_highlighter.get_user_data(block) user_data.clear_data() block_number = block.blockNumber() highlight_errors = lambda cf, ud: cf if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 elif self.migration and (block_number in self.migration.migration_data): highlight_errors = self.__highlight_migration char_format = block.charFormat() char_format = highlight_errors(char_format, user_data) self.setFormat(0, len(block.text()), char_format) for expression, nth, char_format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = len(expression.cap(nth)) char_format = highlight_errors(char_format, user_data) if (self.format(index) != STYLES['string']): self.setFormat(index, length, char_format) if char_format == STYLES['string']: hls.append((index, index + length)) user_data.add_str_group(index, index + length) elif char_format == STYLES['comment']: user_data.comment_start_at(index) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline( text, *self.tri_single, hls=hls, highlight_errors=highlight_errors, user_data=user_data) if not in_multiline: in_multiline = self.match_multiline( text, *self.tri_double, hls=hls, highlight_errors=highlight_errors, user_data=user_data) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) #Highlight selected word if self.selected_word_pattern is not None: index = self.selected_word_pattern.indexIn(text, 0) while index >= 0: index = self.selected_word_pattern.pos(0) length = len(self.selected_word_pattern.cap(0)) char_format = self.format(index) color = STYLES['selectedWord'].foreground().color() color.setAlpha(100) char_format.setBackground(color) self.setFormat(index, length, char_format) index = self.selected_word_pattern.indexIn( text, index + length) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = len(expression.cap(0)) char_format = STYLES['spaces'] char_format = highlight_errors(char_format, user_data) self.setFormat(index, length, char_format) index = expression.indexIn(text, index + length) block.setUserData(user_data)
def realtime_highlight(self, text): """Highlight each line while it is being edited. This function apply the proper highlight to the line being edited by the user, this is a really fast process for each line once you already have the document highlighted, but slow to do it the first time to highlight all the lines together.""" hls = [] block = self.currentBlock() user_data = block.userData() if user_data is None: user_data = SyntaxUserData(False) user_data.clear_data() block_number = block.blockNumber() highlight_errors = lambda cf, ud: cf if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 elif self.migration and (block_number in self.migration.migration_data): highlight_errors = self.__highlight_migration char_format = block.charFormat() char_format = highlight_errors(char_format, user_data) self.setFormat(0, len(block.text()), char_format) for expression, nth, char_format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = len(expression.cap(nth)) char_format = highlight_errors(char_format, user_data) if self.format(index) != STYLES["string"]: self.setFormat(index, length, char_format) if char_format == STYLES["string"]: hls.append((index, index + length)) user_data.add_str_group(index, index + length) elif char_format == STYLES["comment"]: user_data.comment_start_at(index) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline( text, *self.tri_single, hls=hls, highlight_errors=highlight_errors, user_data=user_data ) if not in_multiline: in_multiline = self.match_multiline( text, *self.tri_double, hls=hls, highlight_errors=highlight_errors, user_data=user_data ) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) # Highlight selected word if self.selected_word_pattern is not None: index = self.selected_word_pattern.indexIn(text, 0) while index >= 0: index = self.selected_word_pattern.pos(0) length = len(self.selected_word_pattern.cap(0)) char_format = self.format(index) color = STYLES["selectedWord"].foreground().color() color.setAlpha(100) char_format.setBackground(color) self.setFormat(index, length, char_format) index = self.selected_word_pattern.indexIn(text, index + length) # Spaces expression = QRegExp("\s+") index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = len(expression.cap(0)) char_format = STYLES["spaces"] char_format = highlight_errors(char_format, user_data) self.setFormat(index, length, char_format) index = expression.indexIn(text, index + length) block.setUserData(user_data)
class Highlighter(QSyntaxHighlighter): """Syntax Highlighter for NINJA-IDE.""" # braces braces = ['\\(', '\\)', '\\{', '\\}', '\\[', '\\]'] def __init__(self, document, lang=None, scheme=None, errors=None, pep8=None, migration=None): QSyntaxHighlighter.__init__(self, document) self.highlight_function = self.realtime_highlight self.errors = errors self.pep8 = pep8 self.migration = migration self._old_search = None self.selected_word_lines = [] self.visible_limits = (0, 50) self._styles = {} if lang is not None: self.apply_highlight(lang, scheme) def sanitize(self, word): """Sanitize the string to avoid problems with the regex.""" return word.replace('\\', '\\\\') def apply_highlight(self, lang, scheme=None, syntax=None): """Set the rules that will decide what to highlight and how.""" if syntax is None: langSyntax = settings.SYNTAX.get(lang, {}) else: langSyntax = syntax if scheme is not None: restyle(scheme) keywords = langSyntax.get('keywords', []) operators = langSyntax.get('operators', []) extras = langSyntax.get('extras', []) rules = [] # Keyword, operator, brace and extras rules keyword_pattern = '(^|[^\w\.]{1})(%s)([^\w]{1}|$)' rules += [(keyword_pattern % w, 2, STYLES['keyword']) for w in keywords] rules += [(r'%s' % o, 0, STYLES['operator']) for o in operators] rules += [(r'%s' % b, 0, STYLES['brace']) for b in Highlighter.braces] rules += [(keyword_pattern % e, 2, STYLES['extras']) for e in extras] # All other rules proper = langSyntax.get('properObject', None) if proper is not None: proper = r'\b%s\b' % str(proper[0]) rules += [(proper, 0, STYLES['properObject'])] rules.append((r'__\w+__', 0, STYLES['properObject'])) # Classes and functions definition = langSyntax.get('definition', []) for de in definition: expr = r'\b%s\b\s*(\w+)' % de rules.append((expr, 1, STYLES['definition'])) # Numeric literals rules += [ (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), ] # Regular expressions regex = langSyntax.get('regex', []) for reg in regex: expr = reg[0] color = resources.COLOR_SCHEME['extras'] style = '' if len(reg) > 1: if reg[1] in resources.CUSTOM_SCHEME: color = resources.CUSTOM_SCHEME[reg[1]] elif reg[1] in resources.COLOR_SCHEME: color = resources.COLOR_SCHEME[reg[1]] if len(reg) > 2: style = reg[2] rules.append((expr, 0, format(color, style))) # Strings stringChar = langSyntax.get('string', []) for sc in stringChar: expr = r'"[^"\\]*(\\.[^"\\]*)*"' if sc == '"' \ else r"'[^'\\]*(\\.[^'\\]*)*'" rules.append((expr, 0, STYLES['string'])) # Comments comments = langSyntax.get('comment', []) for co in comments: expr = co + '[^\\n]*' rules.append((expr, 0, STYLES['comment'])) # Multi-line strings (expression, flag, style) # FIXME: The triple-quotes in these two lines will mess up the # syntax highlighting from this point onward self.tri_single = (QRegExp("'''"), 1, STYLES["string2"]) self.tri_double = (QRegExp('"""'), 2, STYLES['string2']) multi = langSyntax.get('multiline_comment', []) if multi: self.multi_start = (QRegExp(re.escape(multi['open'])), STYLES['comment']) self.multi_end = (QRegExp(re.escape(multi['close'])), STYLES['comment']) else: self.multi_start = None # Build a QRegExp for each pattern self.rules = [(QRegExp(pat), index, fmt) for (pat, index, fmt) in rules] self.selected_word_pattern = None #Apply Highlight to the document... (when colors change) self.rehighlight() def set_selected_word(self, word, partial=True): """Set the word to highlight.""" # partial = True for new highlighter compatibility hl_worthy = len(word) > 2 if hl_worthy: self.selected_word_pattern = QRegExp(r'\b%s\b' % self.sanitize(word)) else: self.selected_word_pattern = None suffix = "(?![A-Za-z_\d])" prefix = "(?<![A-Za-z_\d])" word = re.escape(word) if not partial: word = "%s%s%s" % (prefix, word, suffix) lines = [] pat_find = re.compile(word) document = self.document() for lineno, text in enumerate(document.toPlainText().splitlines()): if hl_worthy and pat_find.search(text): lines.append(lineno) elif self._old_search and self._old_search.search(text): lines.append(lineno) # Ask perrito if i don't know what the next line does: self._old_search = hl_worthy and pat_find self.rehighlight_lines(lines) def __highlight_pep8(self, char_format, user_data): """Highlight the lines with pep8 errors.""" user_data.error = True char_format = char_format.toCharFormat() char_format.setUnderlineColor( QColor( resources.CUSTOM_SCHEME.get( 'pep8-underline', resources.COLOR_SCHEME['pep8-underline']))) char_format.setUnderlineStyle(QTextCharFormat.WaveUnderline) return char_format def __highlight_lint(self, char_format, user_data): """Highlight the lines with lint errors.""" user_data.error = True char_format = char_format.toCharFormat() char_format.setUnderlineColor( QColor( resources.CUSTOM_SCHEME.get( 'error-underline', resources.COLOR_SCHEME['error-underline']))) char_format.setUnderlineStyle(QTextCharFormat.WaveUnderline) return char_format def __highlight_migration(self, char_format, user_data): """Highlight the lines with lint errors.""" user_data.error = True char_format = char_format.toCharFormat() char_format.setUnderlineColor( QColor( resources.CUSTOM_SCHEME.get( 'migration-underline', resources.COLOR_SCHEME['migration-underline']))) char_format.setUnderlineStyle(QTextCharFormat.WaveUnderline) return char_format def highlightBlock(self, text): """Apply syntax highlighting to the given block of text.""" self.highlight_function(text) def set_open_visible_area(self, is_line, position): """Set the range of lines that should be highlighted on open.""" if is_line: self.visible_limits = (position - 50, position + 50) def open_highlight(self, text): """Only highlight the lines inside the accepted range.""" if self.visible_limits[0] <= self.currentBlock().blockNumber() <= \ self.visible_limits[1]: self.realtime_highlight(text) else: self.setCurrentBlockState(0) def async_highlight(self): """Execute a thread to collect the info of the things to highlight. The thread will collect the data from where to where to highlight, and which kind of highlight to use for those sections, and return that info to the main thread after it process all the file.""" self.thread_highlight = HighlightParserThread(self) self.connect(self.thread_highlight, SIGNAL("highlightingDetected(PyQt_PyObject)"), self._execute_threaded_highlight) self.thread_highlight.start() def _execute_threaded_highlight(self, styles=None): """Function called with the info collected when the thread ends.""" self.highlight_function = self.threaded_highlight if styles: self._styles = styles lines = list( set(styles.keys()) - set(range(self.visible_limits[0], self.visible_limits[1]))) # Highlight the rest of the lines that weren't highlighted on open self.rehighlight_lines(lines, False) else: self._styles = {} self.highlight_function = self.realtime_highlight self.thread_highlight.wait() def threaded_highlight(self, text): """Highlight each line using the info collected by the thread. This function doesn't need to execute the regular expressions to see where the highlighting starts and end for each rule, it just take the start and end point, and the proper highlighting style from the info returned from the thread and applied that to the document.""" hls = [] block = self.currentBlock() user_data = syntax_highlighter.get_user_data(block) user_data.clear_data() block_number = block.blockNumber() highlight_errors = lambda cf, ud: cf if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 elif self.migration and (block_number in self.migration.migration_data): highlight_errors = self.__highlight_migration char_format = block.charFormat() char_format = highlight_errors(char_format, user_data) self.setFormat(0, len(block.text()), char_format) block_styles = self._styles.get(block.blockNumber(), ()) for index, length, char_format in block_styles: char_format = highlight_errors(char_format, user_data) if (self.format(index) != STYLES['string']): self.setFormat(index, length, char_format) if char_format == STYLES['string']: hls.append((index, index + length)) user_data.add_str_group(index, index + length) elif char_format == STYLES['comment']: user_data.comment_start_at(index) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline( text, *self.tri_single, hls=hls, highlight_errors=highlight_errors, user_data=user_data) if not in_multiline: in_multiline = self.match_multiline( text, *self.tri_double, hls=hls, highlight_errors=highlight_errors, user_data=user_data) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) block.setUserData(user_data) def realtime_highlight(self, text): """Highlight each line while it is being edited. This function apply the proper highlight to the line being edited by the user, this is a really fast process for each line once you already have the document highlighted, but slow to do it the first time to highlight all the lines together.""" hls = [] block = self.currentBlock() user_data = syntax_highlighter.get_user_data(block) user_data.clear_data() block_number = block.blockNumber() highlight_errors = lambda cf, ud: cf if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 elif self.migration and (block_number in self.migration.migration_data): highlight_errors = self.__highlight_migration char_format = block.charFormat() char_format = highlight_errors(char_format, user_data) self.setFormat(0, len(block.text()), char_format) for expression, nth, char_format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = len(expression.cap(nth)) char_format = highlight_errors(char_format, user_data) if (self.format(index) != STYLES['string']): self.setFormat(index, length, char_format) if char_format == STYLES['string']: hls.append((index, index + length)) user_data.add_str_group(index, index + length) elif char_format == STYLES['comment']: user_data.comment_start_at(index) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline( text, *self.tri_single, hls=hls, highlight_errors=highlight_errors, user_data=user_data) if not in_multiline: in_multiline = self.match_multiline( text, *self.tri_double, hls=hls, highlight_errors=highlight_errors, user_data=user_data) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) #Highlight selected word if self.selected_word_pattern is not None: index = self.selected_word_pattern.indexIn(text, 0) while index >= 0: index = self.selected_word_pattern.pos(0) length = len(self.selected_word_pattern.cap(0)) char_format = self.format(index) color = STYLES['selectedWord'].foreground().color() color.setAlpha(100) char_format.setBackground(color) self.setFormat(index, length, char_format) index = self.selected_word_pattern.indexIn( text, index + length) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = len(expression.cap(0)) char_format = STYLES['spaces'] char_format = highlight_errors(char_format, user_data) self.setFormat(index, length, char_format) index = expression.indexIn(text, index + length) block.setUserData(user_data) def _rehighlight_lines(self, lines): """If the document is valid, highlight the list of lines received.""" if self.document() is None: return for line in lines: block = self.document().findBlockByNumber(line) self.rehighlightBlock(block) def _get_errors_lines(self): """Return the number of lines that contains errors to highlight.""" errors_lines = [] block = self.document().begin() while block.isValid(): user_data = syntax_highlighter.get_user_data(block) if user_data.error: errors_lines.append(block.blockNumber()) block = block.next() return errors_lines def rehighlight_lines(self, lines, errors=True): """Rehighlight the lines for errors or selected words.""" if errors: errors_lines = self._get_errors_lines() refresh_lines = set(lines + errors_lines) else: refresh_lines = set(lines + self.selected_word_lines) self.selected_word_lines = lines self._rehighlight_lines(refresh_lines) def match_multiline(self, text, delimiter, in_state, style, hls=[], highlight_errors=lambda x: x, user_data=None): """Do highlighting of multi-line strings. ``delimiter`` should be a ``QRegExp`` for triple-single-quotes or triple-double-quotes, and ``in_state`` should be a unique integer to represent the corresponding state changes when inside those strings. Returns True if we're still inside a multi-line string when this function is finished. """ # If inside triple-single quotes, start at 0 if self.previousBlockState() == in_state: start = 0 add = 0 # Otherwise, look for the delimiter on this line else: start = delimiter.indexIn(text) # Move past this match add = delimiter.matchedLength() # As long as there's a delimiter match on this line... while start >= 0: # Look for the ending delimiter end = delimiter.indexIn(text, start + add) # Ending delimiter on this line? if end >= add: length = end - start + add + delimiter.matchedLength() self.setCurrentBlockState(0) # No; multi-line string else: self.setCurrentBlockState(in_state) length = len(text) - start + add st_fmt = self.format(start) start_collides = [pos for pos in hls if pos[0] < start < pos[1]] # Apply formatting if ((st_fmt != STYLES['comment']) or ((st_fmt == STYLES['comment']) and (self.previousBlockState() != 0))) and \ (len(start_collides) == 0): style = highlight_errors(style, user_data) self.setFormat(start, length, style) else: self.setCurrentBlockState(0) # Look for the next match start = delimiter.indexIn(text, start + length) # Return True if still inside a multi-line string, False otherwise if self.currentBlockState() == in_state: return True else: return False def comment_multiline(self, text, delimiter_end, delimiter_start, style): """Process the beggining and end of a multiline comment.""" startIndex = 0 if self.previousBlockState() != 1: startIndex = delimiter_start.indexIn(text) while startIndex >= 0: endIndex = delimiter_end.indexIn(text, startIndex) commentLength = 0 if endIndex == -1: self.setCurrentBlockState(1) commentLength = len(text) - startIndex else: commentLength = endIndex - startIndex + \ delimiter_end.matchedLength() self.setFormat(startIndex, commentLength, style) startIndex = delimiter_start.indexIn(text, startIndex + commentLength)
def realtime_highlight(self, text): hls = [] block = self.currentBlock() user_data = block.userData() if user_data is None: user_data = SyntaxUserData(False) user_data.clear_data() block_number = block.blockNumber() highlight_errors = lambda cf, ud: cf if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 char_format = block.charFormat() char_format = highlight_errors(char_format, user_data) self.setFormat(0, len(block.text()), char_format) for expression, nth, char_format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = expression.cap(nth).length() char_format = highlight_errors(char_format, user_data) if (self.format(index) != STYLES['string']): self.setFormat(index, length, char_format) if char_format == STYLES['string']: hls.append((index, index + length)) user_data.add_str_group(index, index + length) elif char_format == STYLES['comment']: user_data.comment_start_at(index) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline(text, *self.tri_single, hls=hls, highlight_errors=highlight_errors, user_data=user_data) if not in_multiline: in_multiline = self.match_multiline(text, *self.tri_double, hls=hls, highlight_errors=highlight_errors, user_data=user_data) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) #Highlight selected word if self.selected_word_pattern is not None: index = self.selected_word_pattern.indexIn(text, 0) while index >= 0: index = self.selected_word_pattern.pos(0) length = self.selected_word_pattern.cap(0).length() char_format = self.format(index) color = QColor() color.setNamedColor(STYLES['selectedWord']) color.setAlpha(100) char_format.setBackground(color) self.setFormat(index, length, char_format) index = self.selected_word_pattern.indexIn( text, index + length) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = expression.cap(0).length() char_format = STYLES['spaces'] if settings.HIGHLIGHT_WHOLE_LINE: char_format = highlight_errors(char_format, user_data) self.setFormat(index, length, char_format) index = expression.indexIn(text, index + length) block.setUserData(user_data)
class PPTextEditor(QPlainTextEdit): # Initialize the editor on creation. def __init__(self, parent=None, fontsize=12 ): super(PPTextEditor, self).__init__(parent) # Do not allow line-wrap; horizontal scrollbar appears when required. self.setLineWrapMode(QPlainTextEdit.NoWrap) # make sure when we jump to a line, it goes to the window center self.setCenterOnScroll(True) # Get a monospaced font as selected by the user with View>Font self.setFont(pqMsgs.getMonoFont(fontsize,True)) # instantiate our "syntax" highlighter object, but link it to an empty # QTextDocument. We will redirect it to our actual document only after # loading a document, as it relies on metadata, and then only when/if # the IMC.*HiliteSwitch es are on. self.nulDoc = QTextDocument() # make a null document self.hiliter = wordHighLighter(self.nulDoc) # all the metadata lists will be initialized when self.clear() is # called from pqMain, shortly. # save a regex for quickly finding if a selection is a single word self.oneWordRE = QRegExp(u'^\W*(\w{2,})\W*$') self.menuWord = QString() # Create and initialize an SHA-1 hash machine self.cuisineart = QCryptographicHash(QCryptographicHash.Sha1) # switch on or off our text-highlighting. By switching the highlighter # to a null document we remove highlighting; by switching it back to # the real document, we cause re-highlighting of everything. This makes # significant delay for a large document, so put up a status message # during it by starting and ending a progress bar. def setHighlight(self, onoff): self.hiliter.setDocument(self.nulDoc) # turn off hiliting always if onoff: pqMsgs.showStatusMsg("Setting Scanno/Spelling Highlights...") self.hiliter.setDocument(self.document()) pqMsgs.clearStatusMsg() # Implement clear/new. Just toss everything we keep. def clear(self): self.setHighlight(False) self.document().clear() self.document().setModified(False) self.bookMarkList = \ [None, None, None, None, None, None, None, None, None] IMC.pageTable.clear() IMC.goodWordList.clear() IMC.badWordList.clear() IMC.wordCensus.clear() IMC.charCensus.clear() IMC.notesEditor.clear() IMC.pngPanel.clear() IMC.needSpellCheck = False IMC.needMetadataSave = 0x00 IMC.staleCensus = 0x00 IMC.bookSaveEncoding = QString(u'UTF-8') IMC.bookMainDict = IMC.spellCheck.mainTag # force a cursor "move" in order to create a cursorMoved signal that will # clear the status line - then undo it so the document isn't modified. self.textCursor().insertText(QString(' ')) self.document().undo() # Implement the Edit menu items: # Edit > ToUpper, Edit > ToTitle, Edit > ToLower # Note that in full Unicode, changing letter case is not so simple as it # was in Latin-1! We use the QChar and QString facilities to do it, and # a regex in a loop to pick off words. Restore the current selection after # so another operation can be done on it. # N.B. it is not possible to do self.textCursor().setPosition(), it seems # that self.textCursor() is "const". One has to create a new cursor, # position it, and install it on the document with self.setTextCursor(). def toUpperCase(self): global reWord tc = QTextCursor(self.textCursor()) if not tc.hasSelection() : return # no selection, nothing to do startpos = tc.selectionStart() endpos = tc.selectionEnd() qs = QString(tc.selectedText()) # copy of selected text i = reWord.indexIn(qs,0) # index of first word if any if i < 0 : return # no words in selection, exit while i >= 0: w = reWord.cap(0) # found word as QString n = w.size() # its length qs.replace(i,n,w.toUpper()) # replace it with UC version i = reWord.indexIn(qs,i+n) # find next word if any # we have changed at least one word, replace selection with altered text tc.insertText(qs) # that wiped the selection, so restore it by "dragging" left to right tc.setPosition(startpos,QTextCursor.MoveAnchor) # click tc.setPosition(endpos,QTextCursor.KeepAnchor) # drag self.setTextCursor(tc) # to-lower is identical except for the method call. def toLowerCase(self): global reWord # the regex \b\w+\b tc = QTextCursor(self.textCursor()) if not tc.hasSelection() : return # no selection, nothing to do startpos = tc.selectionStart() endpos = tc.selectionEnd() qs = QString(tc.selectedText()) # copy of selected text i = reWord.indexIn(qs,0) # index of first word if any if i < 0 : return # no words in selection, exit while i >= 0: w = reWord.cap(0) # found word as QString n = w.size() # its length qs.replace(i,n,w.toLower()) # replace it with UC version i = reWord.indexIn(qs,i+n) # find next word if any # we have changed at least one word, replace selection with altered text tc.insertText(qs) # that wiped the selection, so restore it by "dragging" left to right tc.setPosition(startpos,QTextCursor.MoveAnchor) # click tc.setPosition(endpos,QTextCursor.KeepAnchor) # drag self.setTextCursor(tc) # toTitle is similar but we have to change the word to lowercase (in case # it is uppercase now) and then change the initial character to upper. # Note it would be possible to write a smarter version that looked up the # word in a list of common adjectives, connectives, and adverbs and avoided # capitalizing a, and, of, by and so forth. Not gonna happen. def toTitleCase(self): global reWord # the regex \b\w+\b self.toLowerCase() tc = QTextCursor(self.textCursor()) if not tc.hasSelection() : return # no selection, nothing to do startpos = tc.selectionStart() endpos = tc.selectionEnd() qs = QString(tc.selectedText()) # copy of selected text i = reWord.indexIn(qs,0) # index of first word if any if i < 0 : return # no words in selection, exit while i >= 0: w = reWord.cap(0) # found word as QString n = w.size() qs.replace(i,1,qs.at(i).toUpper()) # replace initial with UC i = reWord.indexIn(qs,i+n) # find next word if any # we have changed at least one word, replace selection with altered text tc.insertText(qs) # that wiped the selection, so restore it by "dragging" left to right tc.setPosition(startpos,QTextCursor.MoveAnchor) # click tc.setPosition(endpos,QTextCursor.KeepAnchor) # drag self.setTextCursor(tc) # Re-implement the parent's keyPressEvent in order to provide some # special controls. (Note on Mac, "ctrl-" is "cmd-" and "alt-" is "opt-") # ctrl-plus increases the edit font size 1 pt # (n.b. ctrl-plus probably only comes from a keypad, we usually just get # ctrl-shift-equals instead of plus) # ctrl-minus decreases the edit font size 1 pt # ctrl-<n> for n in 1..9 jumps the insertion point to bookmark <n> # ctrl-shift-<n> extends the selection to bookmark <n> # ctrl-alt-<n> sets bookmark n at the current position def keyPressEvent(self, event): #pqMsgs.printKeyEvent(event) kkey = int( int(event.modifiers()) & IMC.keypadDeModifier) | int(event.key()) # add as little overhead as possible: if it isn't ours, pass it on. if kkey in IMC.keysOfInterest : # we trust python to do this quickly event.accept() # we handle this one if kkey in IMC.findKeys: # ^f, ^g, etc. -- just pass them straight to the Find panel self.emit(SIGNAL("editKeyPress"),kkey) elif kkey in IMC.zoomKeys : # n.b. the self.font and setFont methods inherit from QWidget # Point increment by which to change. n = (-1) if (kkey == IMC.ctl_minus) else 1 # Actual point size currently in use, plus increment p = self.fontInfo().pointSize() + n if (p > 3) and (p < 65): # don't let's get ridiculous, hmm? # Simply calling self.font().setPointSize() had no effect, # we have to actually call setFont() to make change happen. f = self.font() # so get our font, f.setPointSize(p) # change its point size +/- self.setFont(f) # and put the font back IMC.fontSize = p # and remember the size for shutdown time elif kkey in IMC.markKeys : # ^1-9, jump to bookmark bkn = kkey - IMC.ctl_1 # make it 0-8 if self.bookMarkList[bkn] is not None: # if that bookmark is set, self.setTextCursor(self.bookMarkList[bkn]) # jump to it elif kkey in IMC.markShiftKeys : # shift-ctl-1/9, select to mark # Make our document cursor's selection go from our current ANCHOR # to the POSITION from the bookmark cursor. mark_tc = self.bookMarkList[kkey - IMC.ctl_shft_1] if mark_tc is not None: tc = QTextCursor(self.textCursor()) tc.setPosition(mark_tc.position(),QTextCursor.KeepAnchor) self.setTextCursor(tc) elif kkey in IMC.markSetKeys : # ctl-alt-1-9, set a bookmark bkn = kkey - IMC.ctl_alt_1 # make it 0-8 self.bookMarkList[bkn] = QTextCursor(self.textCursor()) IMC.needMetadataSave |= IMC.bookmarksChanged else: # not in keysOfInterest, so pass it up to parent event.ignore() super(PPTextEditor, self).keyPressEvent(event) # Called from pqFind after doing a successful search, this method centers the # current selection (which is the result of the find) in the window. If the selection # is large, put the top of the selection higher than center but on no account # above the top of the viewport. Two problems arise: One, the rectangles returned # by .cursorRect() and by .viewport().geometry() are in pixel units, while the # vertical scrollbar is sized in logical text lines. So we work out the adjustment # as a fraction of the viewport, times the scrollbar's pageStep value to get lines. # Two, cursorRect gives only the height of the actual cursor, not of the selected # text. To find out the height of the full selection we have to get a cursorRect # for the start of the selection, and another for the end of it. def centerCursor(self) : tc = QTextCursor(self.textCursor()) # copy the working cursor with its selection top_point = tc.position() # one end of selection, in character units bot_point = tc.anchor() # ..and the other end if top_point > bot_point : # often the position is > the anchor (top_point, bot_point) = (bot_point, top_point) tc.setPosition(top_point) # cursor for the top of the selection selection_top = self.cursorRect(tc).top() # ..get its top pixel line_height = self.cursorRect(tc).height() # and save height of one line tc.setPosition(bot_point) # cursor for the end of the selection selection_bot = self.cursorRect(tc).bottom() # ..selection's bottom pixel selection_height = selection_bot - selection_top + 1 # selection height in pixels view_height = self.viewport().geometry().height() # scrolled area's height in px view_half = view_height >> 1 # int(view_height/2) pixel_adjustment = 0 if selection_height < view_half : # selected text is less than half the window height: center the top of the # selection, i.e., make the cursor_top equal to view_half. pixel_adjustment = selection_top - view_half # may be negative else : # selected text is taller than half the window, can we show it all? if selection_height < (view_height - line_height) : # all selected text fits in the viewport (with a little free): center it. pixel_adjustment = (selection_top + (selection_height/2)) - view_half else : # not all selected text fits the window, put text top near window top pixel_adjustment = selection_top - line_height # OK, convert the pixel adjustment to a line-adjustment based on the assumption # that a scrollbar pageStep is the height of the viewport in lines. adjust_fraction = pixel_adjustment / view_height vscroller = self.verticalScrollBar() page_step = vscroller.pageStep() # lines in a viewport page, actually less 1 adjust_lines = int(page_step * adjust_fraction) target = vscroller.value() + adjust_lines if (target >= 0) and (target <= vscroller.maximum()) : vscroller.setValue(target) # Catch the contextMenu event and extend the standard context menu with # a separator and the option to add a word to good-words, but only when # there is a selection and it encompasses just one word. def contextMenuEvent(self,event) : ctx_menu = self.createStandardContextMenu() if self.textCursor().hasSelection : qs = self.textCursor().selectedText() if 0 == self.oneWordRE.indexIn(qs) : # it matches at 0 or not at all self.menuWord = self.oneWordRE.cap(1) # save the word ctx_menu.addSeparator() gw_name = QString(self.menuWord) # make a copy gw_action = ctx_menu.addAction(gw_name.append(QString(u' -> Goodwords'))) self.connect(gw_action, SIGNAL("triggered()"), self.addToGW) ctx_menu.exec_(event.globalPos()) # This slot receives the "someword -> good_words" context menu action def addToGW(self) : IMC.goodWordList.insert(self.menuWord) IMC.needMetadataSave |= IMC.goodwordsChanged IMC.needSpellCheck = True IMC.mainWindow.setWinModStatus() # Implement save: the main window opens the files for output using # QIODevice::WriteOnly, which wipes the contents (contrary to the doc) # so we need to write the document and metadata regardless of whether # they've been modified. However we avoid rebuilding metadata if we can. def save(self, dataStream, metaStream): # Get the contents of the document as a QString doc_text = self.toPlainText() # Calculate the SHA-1 hash over the document and save it in both hash # fields of the IMC. self.cuisineart.reset() self.cuisineart.addData(doc_text) IMC.metaHash = IMC.documentHash = bytes(self.cuisineart.result()).__repr__() # write the document, which is pretty simple in the QStream world dataStream << doc_text dataStream.flush() #self.rebuildMetadata() # update any census that needs it self.writeMetadata(metaStream) metaStream.flush() IMC.needMetadataSave = 0x00 self.document().setModified(False) # this triggers main.setWinModStatus() def writeMetadata(self,metaStream): # Writing the metadata takes a bit more work. # pageTable goes out between {{PAGETABLE}}..{{/PAGETABLE}} metaStream << u"{{VERSION 0}}\n" # meaningless at the moment metaStream << u"{{ENCODING " metaStream << unicode(IMC.bookSaveEncoding) metaStream << u"}}\n" metaStream << u"{{STALECENSUS " if 0 == IMC.staleCensus : metaStream << u"FALSE" else: metaStream << u"TRUE" metaStream << u"}}\n" metaStream << u"{{NEEDSPELLCHECK " if 0 == IMC.needSpellCheck : metaStream << u"FALSE" else: metaStream << u"TRUE" metaStream << u"}}\n" metaStream << u"{{MAINDICT " metaStream << unicode(IMC.bookMainDict) metaStream << u"}}\n" # The hash could contain any character. Using __repr__ ensured # it is enclosed in balanced single or double quotes but to be # double sure we will fence it in characters we can spot with a regex. metaStream << u"{{DOCHASH " + IMC.documentHash + u" }}\n" if IMC.pageTable.size() : metaStream << u"{{PAGETABLE}}\n" for i in range(IMC.pageTable.size()) : metaStream << IMC.pageTable.metaStringOut(i) metaStream << u"{{/PAGETABLE}}\n" if IMC.charCensus.size() : metaStream << u"{{CHARCENSUS}}\n" for i in range(IMC.charCensus.size()): (w,n,f) = IMC.charCensus.get(i) metaStream << "{0} {1} {2}\n".format(unicode(w), n, f) metaStream << u"{{/CHARCENSUS}}\n" if IMC.wordCensus.size() : metaStream << u"{{WORDCENSUS}}\n" for i in range(IMC.wordCensus.size()): (w,n,f) = IMC.wordCensus.get(i) metaStream << "{0} {1} {2}\n".format(unicode(w), n, f) metaStream << u"{{/WORDCENSUS}}\n" metaStream << u"{{BOOKMARKS}}\n" for i in range(9): # 0..8 if self.bookMarkList[i] is not None : metaStream << "{0} {1} {2}\n".format(i,self.bookMarkList[i].position(),self.bookMarkList[i].anchor()) metaStream << u"{{/BOOKMARKS}}\n" metaStream << u"{{NOTES}}\n" d = IMC.notesEditor.document() if not d.isEmpty(): for i in range( d.blockCount() ): t = d.findBlockByNumber(i).text() if t.startsWith("{{"): t.prepend(u"\xfffd") # Unicode Replacement char metaStream << t + "\n" IMC.notesEditor.document().setModified(False) metaStream << u"{{/NOTES}}\n" if IMC.goodWordList.active() : # have some good words metaStream << u"{{GOODWORDS}}\n" IMC.goodWordList.save(metaStream) metaStream << u"{{/GOODWORDS}}\n" if IMC.badWordList.active() : # have some bad words metaStream << u"{{BADWORDS}}\n" IMC.badWordList.save(metaStream) metaStream << u"{{/BADWORDS}}\n" p1 = self.textCursor().selectionStart() p2 = self.textCursor().selectionEnd() metaStream << u"{{CURSOR "+unicode(p1)+u' '+unicode(p2)+u"}}\n" metaStream.flush() # Implement load: the main window has the job of finding and opening files # then passes QTextStreams ready to read here. If metaStream is None, # no metadata file was found and we construct the metadata. # n.b. before main calls here, it calls our .clear, hence lists are # empty, hiliting is off, etc. def load(self, dataStream, metaStream, goodStream, badStream): # Load the document file into the editor self.setPlainText(dataStream.readAll()) # Initialize the hash value for the document, which will be equal unless # we read something different from the metadata file. self.cuisineart.reset() self.cuisineart.addData(self.toPlainText()) IMC.metaHash = IMC.documentHash = bytes(self.cuisineart.result()).__repr__() if metaStream is None: # load goodwords, badwords, and take census if goodStream is not None: IMC.goodWordList.load(goodStream) if badStream is not None: IMC.badWordList.load(badStream) self.rebuildMetadata(page=True) # build page table & vocab from scratch else: self.loadMetadata(metaStream) # If the metaData and document hashes now disagree, it is because the metadata # had a DOCHASH value for a different book or version. Warn the user. if IMC.metaHash != IMC.documentHash : pqMsgs.warningMsg(u"The document file and metadata file do not match!", u"Bookmarks, page breaks and other metadata will be wrong! Strongly recommend you not edit or save this book.") # restore hiliting if the user wanted it. Note this can cause a # serious delay if the new book is large. However the alternative is # to not set it on and then we are out of step with the View menu # toggles, so the user has to set it off before loading, or suffer. self.setHighlight(IMC.scannoHiliteSwitch or IMC.spellingHiliteSwitch) # set a different main dict if there was one in the metadata if IMC.bookMainDict is not None: IMC.spellCheck.setMainDict(IMC.bookMainDict) # load page table & vocab from the .meta file as a stream. # n.b. QString has a split method we could use but instead # we take the input line to a Python u-string and split it. For # the word/char census we have to take the key back to a QString. def loadMetadata(self,metaStream): sectionRE = QRegExp( u"\{\{(" + '|'.join ( ['PAGETABLE','CHARCENSUS','WORDCENSUS','BOOKMARKS', 'NOTES','GOODWORDS','BADWORDS','CURSOR','VERSION', 'STALECENSUS','NEEDSPELLCHECK','ENCODING', 'DOCHASH', 'MAINDICT'] ) \ + u")(.*)\}\}", Qt.CaseSensitive) metaVersion = 0 # base version while not metaStream.atEnd() : qline = metaStream.readLine().trimmed() if qline.isEmpty() : continue # allow blank lines between sections if sectionRE.exactMatch(qline) : # section start section = sectionRE.cap(1) argument = unicode(sectionRE.cap(2).trimmed()) endsec = QString(u"{{/" + section + u"}}") if section == u"VERSION": if len(argument) != 0 : metaVersion = int(argument) continue # no more data after {{VERSION x }} elif section == u"STALECENSUS" : if argument == u"TRUE" : IMC.staleCensus = IMC.staleCensusLoaded continue # no more data after {{STALECENSUS x}} elif section == u"NEEDSPELLCHECK" : if argument == u"TRUE" : IMC.needSpellCheck = True continue # no more data after {{NEEDSPELLCHECK x}} elif section == u"ENCODING" : IMC.bookSaveEncoding = QString(argument) continue elif section == u"MAINDICT" : IMC.bookMainDict = QString(argument) continue elif section == u"DOCHASH" : IMC.metaHash = argument continue elif section == u"PAGETABLE": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and (not qline.isEmpty()): IMC.pageTable.metaStringIn(qline) qline = metaStream.readLine() continue elif section == u"CHARCENSUS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and (not qline.isEmpty()): # can't just .split the char census, the first # char is the char being counted and it can be a space. str = unicode(qline) parts = str[2:].split(' ') IMC.charCensus.append(QString(str[0]),int(parts[0]),int(parts[1])) qline = metaStream.readLine() continue elif section == u"WORDCENSUS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and (not qline.isEmpty()): parts = unicode(qline).split(' ') IMC.wordCensus.append(QString(parts[0]),int(parts[1]),int(parts[2])) qline = metaStream.readLine() continue elif section == u"BOOKMARKS": qline = metaStream.readLine() while (not qline.startsWith(endsec)) and (not qline.isEmpty()): parts = unicode(qline).split(' ') tc = QTextCursor(self.document() ) tc.setPosition(int(parts[1])) if len(parts) == 3 : # early versions didn't save anchor tc.movePosition(int(parts[2]),QTextCursor.KeepAnchor) self.bookMarkList[int(parts[0])] = tc qline = metaStream.readLine() continue elif section == u"NOTES": e = IMC.notesEditor e.setUndoRedoEnabled(False) qline = metaStream.readLine() while (not qline.startsWith(endsec)) and not metaStream.atEnd(): if qline.startsWith(u"\xfffd"): # escaped {{ qline.remove(0,1) e.appendPlainText(qline) qline = metaStream.readLine() e.setUndoRedoEnabled(True) continue elif section == u"GOODWORDS" : # not going to bother checking for endsec return, # if it isn't that then we will shortly fail anyway w = IMC.goodWordList.load(metaStream,endsec) continue elif section == u"BADWORDS" : w = IMC.badWordList.load(metaStream,endsec) continue elif section == u"CURSOR" : # restore selection as of save p1p2 = argument.split(' ') tc = QTextCursor(self.document()) tc.setPosition(int(p1p2[0]),QTextCursor.MoveAnchor) tc.setPosition(int(p1p2[1]),QTextCursor.KeepAnchor) self.setTextCursor(tc) else: # this can't happen; section is text captured by the RE # and we have accounted for all possibilities raise AssertionError, "impossible metadata" else: # Non-blank line that doesn't match sectionRE? pqMsgs.infoMsg( "Unexpected line in metadata: {0}".format(pqMsgs.trunc(qline,20)), "Metadata may be incomplete, suggest quit") break # Rebuild as much of the char/word census and spellcheck as we need to. # This is called from load, above, and from the Char and Word panels # Refresh buttons. If page=True we are loading a doc for which there is # no metadata file, so cache page definitions; otherwise just skip the # page definitions (see doCensus). If the doc has changed we need to # rerun the full char/word census. But if not, we might still need a # spellcheck, if the dictionary has changed. def rebuildMetadata(self,page=False): if page or (0 != IMC.staleCensus) : self.doCensus(page) if IMC.needSpellCheck : self.doSpellcheck() # Go through vocabulary census and check the spelling (it would be a big # waste of time to check every word as it was read). If the spellcheck # is not up (i.e. it couldn't find a dictionary) we only mark as bad the # words in the badwords list. def doSpellcheck(self): canspell = IMC.spellCheck.isUp() nwords = IMC.wordCensus.size() if 0 >= nwords : # could be zero in a null document return pqMsgs.startBar(nwords,"Checking spelling...") for i in range(IMC.wordCensus.size()): (qword, cnt, wflags) = IMC.wordCensus.get(i) wflags = wflags & (0xff - IMC.WordMisspelt) # turn off flag if on # some words have /dict-tag, split that out as string or "" (w,x,d) = unicode(qword).partition("/") if IMC.goodWordList.check(w): pass elif IMC.badWordList.check(w) : wflags |= IMC.WordMisspelt elif canspell : # check word in its optional dictionary if not ( IMC.spellCheck.check(w,d) ) : wflags |= IMC.WordMisspelt IMC.wordCensus.setflags(i,wflags) if 0 == i & 0x1f : pqMsgs.rollBar(i) pqMsgs.endBar() IMC.needMetadataSave |= IMC.wordlistsChanged IMC.needSpellCheck = False if IMC.spellingHiliteSwitch : self.setHighlight(True) # force refresh of spell underlines # Scan the successive lines of the document and build the census of chars, # words, and (first time only) the table of page separators. # # If this is an HTML file (from IMC.bookType), and if its first line is # <!DOCTYPE..., we skip until we see <body>. This avoids polluting our # char and word censii with CSS comments and etc. Regular HTML tags # like <table> and <b> are skipped over automatically during parsing. # # Qt obligingly supplies each line as a QTextBlock. We examine the line # to see if it is a page separator. If we are opening a file having no # metadata, the Page argument is True and we build a page table entry. # Other times (e.g. from the Refresh button of the Word or Char panel), # we skip over page separator lines. # Each non-separator line is first scanned by characters and then for words. # The character scan counts characters for the Chars panel. We do NOT parse # the text for PGDP productions [oe] and [OE] nor other markups for accented # characters such as [=o] for o-with-macron or [^a] for a-with-circumflex. # These are just counted as [, o, e, ]. Reasons: (1) the alternative, to parse # them into their proper unicode values and count those, entails a whole lotta # code that would slow this census badly; (2) having the unicode chars in # the Chars panel would be confusing when they are not actually in the text; # (3) there is some value in having the counts of [ and ]. For similar reasons # we count all the chars in HTML e.g. "<i>" is three characters even though it # is effectively unprinted metadata. # In scanning words, we collect numbers as words. We collect internal hyphens # as letters ("mother-in-law") but not at end of word ("help----" or emdash). # We collect internal apostrophes ("it's", "hadn't") but not apostrophes at ends, # "'Twas" is counted as "Twas", "students' work" as "students work". This is because # there seems to be no way to distinguish the contractive prefix ('Twas) # and the final possessive (students') from normal single-quote marks! # And we collect leading and internal, but not trailing, square brackets as # letters. Thus [OE]dipus and ma[~n]ana are words (but will fail spellcheck) # while Einstein[A] (a footnote key) is not. # We also collect HTML productions ("</i>" and "<table>") as words. They do not # go in the census but we check them for lang= attributes and set the alternate # spellcheck dictionary from them. def doCensus(self, page=False) : global reLineSep, reTokens, reLang, qcLess # Clear the current census values IMC.wordCensus.clear() IMC.charCensus.clear() # Count chars locally for speed local_char_census = defaultdict(int) # Name of current alternate dictionary alt_dict = QString() # isEmpty when none # Tag from which we set an alternate dict alt_dict_tag = QString() # Start the progress bar based on the number of lines in the document pqMsgs.startBar(self.document().blockCount(),"Counting words and chars...") # Find the first text block of interest, skipping an HTML header file qtb = self.document().begin() # first text block if IMC.bookType.startsWith(QString(u"htm")) \ and qtb.text().startsWith(QString(u"<!DOCTYPE")) : while (qtb != self.document().end()) \ and (not qtb.text().startsWith(QString(u"<body"))) : qtb = qtb.next() # Scan all lines of the document to the end. while qtb != self.document().end() : qsLine = qtb.text() # text of line as qstring dbg = qsLine.size() dbg2 = qtb.length() if reLineSep.exactMatch(qsLine): # this is a page separator line if page : # We are doing page seps, it's for Open with no .meta seen, # the page table has been cleared. Store the page sep # data in the page table, with a textCursor to its start. qsfilenum = reLineSep.cap(1) # xxx from "File: xxx.png" qsproofers = reLineSep.cap(2) # \who\x\blah\etc # proofer names can contain spaces, replace with en-space char qsproofers.replace(QChar(" "),QChar(0x2002)) # create a new TextCursor instance tcursor = QTextCursor(self.document()) # point it to this text block tcursor.setPosition(qtb.position()) # dump all that in the page table IMC.pageTable.loadPsep(tcursor, qsfilenum, qsproofers) # else not doing pages, just ignore this psep line else: # not psep, ordinary text line, count chars and words pyLine = unicode(qsLine) # move into Python space to count for c in pyLine : local_char_census[c] += 1 j = 0 while True: j = reTokens.indexIn(qsLine,j) if j < 0 : # no more word-like units break qsWord = reTokens.cap(0) j += qsWord.size() if qsWord.startsWith(qcLess) : # Examine a captured HTML production. if not reTokens.cap(2).isEmpty() : # HTML open tag, look for lang='dict' if 0 <= reLang.indexIn(reTokens.cap(3)) : # found it: save tag and dict name alt_dict_tag = QString(reTokens.cap(2)) alt_dict = QString(reLang.cap(1)) alt_dict.prepend(u'/') # make "/en_GB" # else no lang= attribute else: # HTML close tag, see if it closes alt dict use if reTokens.cap(5) == alt_dict_tag : # yes, matches open-tag for dict, clear it alt_dict_tag = QString() alt_dict = QString() # else no alt dict in use, or didn't match else : # did not start with "<", process as a word # Set the property flags, which is harder now we don't # look at every character. Use the QString facilities # rather than python because python .isalnum fails # for a hyphenated number "1850-1910". flag = 0 if 0 != qsWord.compare(qsWord.toLower()) : flag |= IMC.WordHasUpper if 0 != qsWord.compare(qsWord.toUpper()) : flag |= IMC.WordHasLower if qsWord.contains(qcHyphen) : flag |= IMC.WordHasHyphen if qsWord.contains(qcApostrophe) or qsWord.contains(qcCurlyApostrophe) : flag |= IMC.WordHasApostrophe if qsWord.contains(reDigit) : flag |= IMC.WordHasDigit IMC.wordCensus.count(qsWord.append(alt_dict),flag) # end "while any more words in this line" # end of not-a-psep-line processing qtb = qtb.next() # move on to next block if (0 == (qtb.blockNumber() & 255)) : #every 256th block pqMsgs.rollBar(qtb.blockNumber()) # roll the bar QApplication.processEvents() # end of scanning all text blocks in the doc pqMsgs.endBar() # we accumulated the char counts in localCharCensus. Now read it out # in sorted order and stick it in the IMC.charCensus list. for one_char in sorted(local_char_census.keys()): qc = QChar(ord(one_char)) # get to QChar for category() method IMC.charCensus.append(QString(qc),local_char_census[one_char],qc.category()) IMC.needSpellCheck = True # after a census this is true IMC.staleCensus = 0 # but this is no longer true IMC.needMetadataSave |= IMC.wordlistsChanged
class Highlighter (QSyntaxHighlighter): # braces braces = ['\\(', '\\)', '\\{', '\\}', '\\[', '\\]'] def __init__(self, document, lang=None, scheme=None, errors=None, pep8=None): QSyntaxHighlighter.__init__(self, document) self.errors = errors self.pep8 = pep8 if lang is not None: self.apply_highlight(lang, scheme) def apply_highlight(self, lang, scheme=None, syntax=None): if syntax is None: langSyntax = settings.SYNTAX.get(lang, {}) else: langSyntax = syntax if scheme: restyle(scheme) keywords = langSyntax.get('keywords', []) operators = langSyntax.get('operators', []) extras = langSyntax.get('extras', []) rules = [] # Keyword, operator, brace and extras rules rules += [(r'\b%s\b' % w, 0, STYLES['keyword']) for w in keywords] rules += [(r'%s' % o, 0, STYLES['operator']) for o in operators] rules += [(r'%s' % b, 0, STYLES['brace']) for b in Highlighter.braces] rules += [(r'\b%s\b' % e, 0, STYLES['extras']) for e in extras] # All other rules proper = langSyntax.get('properObject', None) if proper is not None: proper = '\\b' + str(proper[0]) + '\\b' rules += [(proper, 0, STYLES['properObject'])] rules.append((r'__\w+__', 0, STYLES['properObject'])) definition = langSyntax.get('definition', []) for de in definition: expr = '\\b' + de + '\\b\\s*(\\w+)' rules.append((expr, 1, STYLES['definition'])) # Numeric literals rules += [ (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), ] regex = langSyntax.get('regex', []) for reg in regex: expr = reg[0] color = resources.COLOR_SCHEME['extras'] style = '' if len(reg) > 1: if reg[1] in resources.CUSTOM_SCHEME: color = resources.CUSTOM_SCHEME[reg[1]] elif reg[1] in resources.COLOR_SCHEME: color = resources.COLOR_SCHEME[reg[1]] if len(reg) > 2: style = reg[2] rules.append((expr, 0, format(color, style))) stringChar = langSyntax.get('string', []) for sc in stringChar: expr = r'"[^"\\]*(\\.[^"\\]*)*"' if sc == '"' \ else r"'[^'\\]*(\\.[^'\\]*)*'" rules.append((expr, 0, STYLES['string'])) comments = langSyntax.get('comment', []) for co in comments: expr = co + '[^\\n]*' rules.append((expr, 0, STYLES['comment'])) # Multi-line strings (expression, flag, style) # FIXME: The triple-quotes in these two lines will mess up the # syntax highlighting from this point onward self.tri_single = (QRegExp("'''"), 1, STYLES["string2"]) self.tri_double = (QRegExp('"""'), 2, STYLES['string2']) multi = langSyntax.get('multiline_comment', []) if multi: self.multi_start = (QRegExp(multi['open']), STYLES['comment']) self.multi_end = (QRegExp(multi['close']), STYLES['comment']) else: self.multi_start = None # Build a QRegExp for each pattern self.rules = [(QRegExp(pat), index, fmt) for (pat, index, fmt) in rules] self.selected_word_pattern = None #Apply Highlight to the document... (when colors change) self.rehighlight() def set_selected_word(self, word): """Set the word to highlight.""" if len(word) > 2: self.selected_word_pattern = QRegExp(r'\b%s\b' % word) else: self.selected_word_pattern = None def __highlight_pep8(self, format): """Highlight the lines with errors.""" format = format.toCharFormat() format.setUnderlineColor(QColor( resources.CUSTOM_SCHEME.get('pep8-underline', resources.COLOR_SCHEME['pep8-underline']))) format.setUnderlineStyle( QTextCharFormat.WaveUnderline) return format def __highlight_lint(self, format): """Highlight the lines with errors.""" format = format.toCharFormat() format.setUnderlineColor(QColor( resources.CUSTOM_SCHEME.get('error-underline', resources.COLOR_SCHEME['error-underline']))) format.setUnderlineStyle( QTextCharFormat.WaveUnderline) return format def highlightBlock(self, text): """Apply syntax highlighting to the given block of text.""" hls = [] block = self.currentBlock() block_number = self.currentBlock().blockNumber() highlight_errors = lambda x: x if self.errors and (block_number in self.errors.errorsSummary): highlight_errors = self.__highlight_lint elif self.pep8 and (block_number in self.pep8.pep8checks): highlight_errors = self.__highlight_pep8 format = block.charFormat() format = highlight_errors(format) self.setFormat(0, len(block.text()), format) for expression, nth, format in self.rules: index = expression.indexIn(text, 0) while index >= 0: # We actually want the index of the nth match index = expression.pos(nth) length = expression.cap(nth).length() format = highlight_errors(format) if (self.format(index) != STYLES['string']): self.setFormat(index, length, format) if format == STYLES['string']: hls.append((index, index + length)) index = expression.indexIn(text, index + length) self.setCurrentBlockState(0) if not self.multi_start: # Do multi-line strings in_multiline = self.match_multiline(text, *self.tri_single, hls=hls) if not in_multiline: in_multiline = self.match_multiline(text, *self.tri_double, hls=hls) else: # Do multi-line comment self.comment_multiline(text, self.multi_end[0], *self.multi_start) #Highlight selected word if self.selected_word_pattern is not None: index = self.selected_word_pattern.indexIn(text, 0) while index >= 0: index = self.selected_word_pattern.pos(0) length = self.selected_word_pattern.cap(0).length() format = self.format(index) color = QColor() color.setNamedColor(STYLES['selectedWord']) color.setAlpha(100) format.setBackground(color) self.setFormat(index, length, format) index = self.selected_word_pattern.indexIn( text, index + length) #Spaces expression = QRegExp('\s+') index = expression.indexIn(text, 0) while index >= 0: index = expression.pos(0) length = expression.cap(0).length() format = STYLES['spaces'] format = highlight_errors(format) self.setFormat(index, length, format) index = expression.indexIn(text, index + length) def match_multiline(self, text, delimiter, in_state, style, hls=[]): """Do highlighting of multi-line strings. ``delimiter`` should be a ``QRegExp`` for triple-single-quotes or triple-double-quotes, and ``in_state`` should be a unique integer to represent the corresponding state changes when inside those strings. Returns True if we're still inside a multi-line string when this function is finished. """ # If inside triple-single quotes, start at 0 if self.previousBlockState() == in_state: start = 0 add = 0 # Otherwise, look for the delimiter on this line else: start = delimiter.indexIn(text) # Move past this match add = delimiter.matchedLength() # As long as there's a delimiter match on this line... while start >= 0: # Look for the ending delimiter end = delimiter.indexIn(text, start + add) # Ending delimiter on this line? if end >= add: length = end - start + add + delimiter.matchedLength() self.setCurrentBlockState(0) # No; multi-line string else: self.setCurrentBlockState(in_state) length = text.length() - start + add st_fmt = self.format(start) start_collides = [pos for pos in hls if pos[0] < start < pos[1]] # Apply formatting if ((st_fmt != STYLES['comment']) or \ ((st_fmt == STYLES['comment']) and (self.previousBlockState() != 0))) and \ (len(start_collides) == 0): self.setFormat(start, length, style) else: self.setCurrentBlockState(0) # Look for the next match start = delimiter.indexIn(text, start + length) # Return True if still inside a multi-line string, False otherwise if self.currentBlockState() == in_state: return True else: return False def comment_multiline(self, text, delimiter_end, delimiter_start, style): startIndex = 0 if self.previousBlockState() != 1: startIndex = delimiter_start.indexIn(text) while startIndex >= 0: endIndex = delimiter_end.indexIn(text, startIndex) commentLength = 0 if endIndex == -1: self.setCurrentBlockState(1) commentLength = text.length() - startIndex else: commentLength = endIndex - startIndex + \ delimiter_end.matchedLength() self.setFormat(startIndex, commentLength, style) startIndex = delimiter_start.indexIn(text, startIndex + commentLength)