Edit Book: 2x speedup for inline spell checking. Useful if you are editing large HTML files (over 100KB in size).

Qt's text layout engine becomes very slow if we store arbitrary python
objects in QTextCharFormat classes. So refactor to not do that.
This commit is contained in:
Kovid Goyal 2014-06-23 15:19:10 +05:30
parent 81e6ed1551
commit 58203bbaa5
5 changed files with 68 additions and 31 deletions

View File

@ -35,6 +35,7 @@ def editor_from_syntax(syntax, parent=None):
SYNTAX_PROPERTY = QTextCharFormat.UserProperty SYNTAX_PROPERTY = QTextCharFormat.UserProperty
SPELL_PROPERTY = SYNTAX_PROPERTY + 1 SPELL_PROPERTY = SYNTAX_PROPERTY + 1
SPELL_LOCALE_PROPERTY = SPELL_PROPERTY + 1
class SyntaxTextCharFormat(QTextCharFormat): class SyntaxTextCharFormat(QTextCharFormat):
@ -47,4 +48,16 @@ class SyntaxTextCharFormat(QTextCharFormat):
id(self), self.foreground().color().name(), self.fontItalic(), self.fontWeight() >= QFont.DemiBold) id(self), self.foreground().color().name(), self.fontItalic(), self.fontWeight() >= QFont.DemiBold)
__str__ = __repr__ __str__ = __repr__
class StoreLocale(object):
__slots__ = ('enabled',)
def __init__(self):
self.enabled = False
def __enter__(self):
self.enabled = True
def __exit__(self, *args):
self.enabled = False
store_locale = StoreLocale()

View File

@ -129,28 +129,32 @@ class SyntaxHighlighter(object):
try: try:
block = doc.findBlock(position) block = doc.findBlock(position)
while block.isValid() and (block.position() < end_pos or force_next_highlight): while block.isValid() and (block.position() < end_pos or force_next_highlight):
ud, new_ud = self.get_user_data(block) formats, force_next_highlight = self.parse_single_block(block)
orig_state = ud.state
pblock = block.previous()
if pblock.isValid():
start_state = pblock.userData()
if start_state is None:
start_state = self.user_data_factory().state
else:
start_state = start_state.state.copy()
else:
start_state = self.user_data_factory().state
ud.clear(state=start_state) # Ensure no stale user data lingers
formats = []
for i, num, fmt in run_loop(ud, self.state_map, self.formats, unicode(block.text())):
if fmt is not None:
formats.append((i, num, fmt))
self.apply_format_changes(doc, block, formats) self.apply_format_changes(doc, block, formats)
force_next_highlight = new_ud or ud.state != orig_state
block = block.next() block = block.next()
finally: finally:
doc.contentsChange.connect(self.reformat_blocks) doc.contentsChange.connect(self.reformat_blocks)
def parse_single_block(self, block):
ud, new_ud = self.get_user_data(block)
orig_state = ud.state
pblock = block.previous()
if pblock.isValid():
start_state = pblock.userData()
if start_state is None:
start_state = self.user_data_factory().state
else:
start_state = start_state.state.copy()
else:
start_state = self.user_data_factory().state
ud.clear(state=start_state) # Ensure no stale user data lingers
formats = []
for i, num, fmt in run_loop(ud, self.state_map, self.formats, unicode(block.text())):
if fmt is not None:
formats.append((i, num, fmt))
force_next_highlight = new_ud or ud.state != orig_state
return formats, force_next_highlight
def reformat_block(self, block): def reformat_block(self, block):
if block.isValid(): if block.isValid():
self.reformat_blocks(block.position(), 0, 1) self.reformat_blocks(block.position(), 0, 1)

View File

@ -16,7 +16,7 @@ from calibre.ebooks.oeb.polish.spell import html_spell_tags, xml_spell_tags
from calibre.spell.dictionary import parse_lang_code from calibre.spell.dictionary import parse_lang_code
from calibre.spell.break_iterator import split_into_words_and_positions from calibre.spell.break_iterator import split_into_words_and_positions
from calibre.gui2.tweak_book import dictionaries, tprefs from calibre.gui2.tweak_book import dictionaries, tprefs
from calibre.gui2.tweak_book.editor import SyntaxTextCharFormat, SPELL_PROPERTY from calibre.gui2.tweak_book.editor import SyntaxTextCharFormat, SPELL_PROPERTY, SPELL_LOCALE_PROPERTY, store_locale
from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter, run_loop from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter, run_loop
from calibre.gui2.tweak_book.editor.syntax.css import ( from calibre.gui2.tweak_book.editor.syntax.css import (
create_formats as create_css_formats, state_map as css_state_map, CSSState, CSSUserData) create_formats as create_css_formats, state_map as css_state_map, CSSState, CSSUserData)
@ -235,9 +235,12 @@ def check_spelling(text, tpos, tlen, fmt, locale, sfmt):
if recognized: if recognized:
split_ans.append((length, fmt)) split_ans.append((length, fmt))
else: else:
wsfmt = SyntaxTextCharFormat(sfmt) if store_locale.enabled:
wsfmt.setProperty(SPELL_PROPERTY, (ctext[start:ppos], locale)) s = SyntaxTextCharFormat(sfmt)
split_ans.append((length, wsfmt)) s.setProperty(SPELL_LOCALE_PROPERTY, locale)
split_ans.append((length, s))
else:
split_ans.append((length, sfmt))
if ppos < tlen: if ppos < tlen:
split_ans.append((tlen - ppos, fmt)) split_ans.append((tlen - ppos, fmt))
return split_ans return split_ans
@ -486,6 +489,7 @@ def create_formats(highlighter, add_css=True):
f.setFontWeight(QFont.Bold) f.setFontWeight(QFont.Bold)
if add_css: if add_css:
formats['css_sub_formats'] = create_css_formats(highlighter) formats['css_sub_formats'] = create_css_formats(highlighter)
formats['spell'].setProperty(SPELL_PROPERTY, True)
return formats return formats

View File

@ -18,7 +18,7 @@ from PyQt4.Qt import (
from calibre import prepare_string_for_xml, xml_entity_to_unicode from calibre import prepare_string_for_xml, xml_entity_to_unicode
from calibre.gui2.tweak_book import tprefs, TOP from calibre.gui2.tweak_book import tprefs, TOP
from calibre.gui2.tweak_book.editor import SYNTAX_PROPERTY, SPELL_PROPERTY from calibre.gui2.tweak_book.editor import SYNTAX_PROPERTY, SPELL_PROPERTY, SPELL_LOCALE_PROPERTY, store_locale
from calibre.gui2.tweak_book.editor.themes import get_theme, theme_color, theme_format from calibre.gui2.tweak_book.editor.themes import get_theme, theme_color, theme_format
from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter
from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter
@ -427,7 +427,7 @@ class TextEdit(PlainTextEdit):
block = c.block() block = c.block()
while block.isValid(): while block.isValid():
for r in block.layout().additionalFormats(): for r in block.layout().additionalFormats():
if r.format.property(SPELL_PROPERTY).toPyObject() is not None: if r.format.property(SPELL_PROPERTY).toBool():
if not from_cursor or block.position() + r.start + r.length > c.position(): if not from_cursor or block.position() + r.start + r.length > c.position():
c.setPosition(block.position() + r.start) c.setPosition(block.position() + r.start)
c.setPosition(c.position() + r.length, c.KeepAnchor) c.setPosition(c.position() + r.length, c.KeepAnchor)
@ -568,26 +568,42 @@ class TextEdit(PlainTextEdit):
return False return False
return QPlainTextEdit.event(self, ev) return QPlainTextEdit.event(self, ev)
def text_for_range(self, block, r):
c = self.textCursor()
c.setPosition(block.position() + r.start)
c.setPosition(c.position() + r.length, c.KeepAnchor)
return unicode(c.selectedText())
def spellcheck_locale_for_cursor(self, c):
with store_locale:
formats = self.highlighter.parse_single_block(c.block())[0]
pos = c.positionInBlock()
for i, num, fmt in formats:
if i <= pos < i + num and fmt.property(SPELL_PROPERTY).toBool():
return fmt.property(SPELL_LOCALE_PROPERTY).toPyObject()
def recheck_word(self, word, locale): def recheck_word(self, word, locale):
c = self.textCursor() c = self.textCursor()
c.movePosition(c.Start) c.movePosition(c.Start)
block = c.block() block = c.block()
while block.isValid(): while block.isValid():
for r in block.layout().additionalFormats(): for r in block.layout().additionalFormats():
x = r.format.property(SPELL_PROPERTY).toPyObject() if r.format.property(SPELL_PROPERTY).toBool() and self.text_for_range(block, r) == word:
if x is not None and word == x[0]:
self.highlighter.reformat_block(block) self.highlighter.reformat_block(block)
break break
block = block.next() block = block.next()
# Tooltips {{{ # Tooltips {{{
def syntax_format_for_cursor(self, cursor): def syntax_range_for_cursor(self, cursor):
if cursor.isNull(): if cursor.isNull():
return return
pos = cursor.positionInBlock() pos = cursor.positionInBlock()
for r in cursor.block().layout().additionalFormats(): for r in cursor.block().layout().additionalFormats():
if r.start <= pos < r.start + r.length and r.format.property(SYNTAX_PROPERTY).toBool(): if r.start <= pos < r.start + r.length and r.format.property(SYNTAX_PROPERTY).toBool():
return r.format return r
def syntax_format_for_cursor(self, cursor):
return getattr(self.syntax_range_for_cursor(cursor), 'format', None)
def show_tooltip(self, ev): def show_tooltip(self, ev):
c = self.cursorForPosition(ev.pos()) c = self.cursorForPosition(ev.pos())

View File

@ -383,10 +383,10 @@ class Editor(QMainWindow):
m = QMenu(self) m = QMenu(self)
a = m.addAction a = m.addAction
c = self.editor.cursorForPosition(pos) c = self.editor.cursorForPosition(pos)
fmt = self.editor.syntax_format_for_cursor(c) r = self.editor.syntax_range_for_cursor(c)
spell = fmt.property(SPELL_PROPERTY).toPyObject() if fmt is not None else None if r.format.property(SPELL_PROPERTY).toBool():
if spell is not None: word = self.editor.text_for_range(c.block(), r)
word, locale = spell locale = self.editor.spellcheck_locale_for_cursor(c)
orig_pos = c.position() orig_pos = c.position()
c.setPosition(orig_pos - utf16_length(word)) c.setPosition(orig_pos - utf16_length(word))
found = False found = False