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
SPELL_PROPERTY = SYNTAX_PROPERTY + 1
SPELL_LOCALE_PROPERTY = SPELL_PROPERTY + 1
class SyntaxTextCharFormat(QTextCharFormat):
@ -47,4 +48,16 @@ class SyntaxTextCharFormat(QTextCharFormat):
id(self), self.foreground().color().name(), self.fontItalic(), self.fontWeight() >= QFont.DemiBold)
__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:
block = doc.findBlock(position)
while block.isValid() and (block.position() < end_pos or force_next_highlight):
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))
formats, force_next_highlight = self.parse_single_block(block)
self.apply_format_changes(doc, block, formats)
force_next_highlight = new_ud or ud.state != orig_state
block = block.next()
finally:
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):
if block.isValid():
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.break_iterator import split_into_words_and_positions
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.css import (
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:
split_ans.append((length, fmt))
else:
wsfmt = SyntaxTextCharFormat(sfmt)
wsfmt.setProperty(SPELL_PROPERTY, (ctext[start:ppos], locale))
split_ans.append((length, wsfmt))
if store_locale.enabled:
s = SyntaxTextCharFormat(sfmt)
s.setProperty(SPELL_LOCALE_PROPERTY, locale)
split_ans.append((length, s))
else:
split_ans.append((length, sfmt))
if ppos < tlen:
split_ans.append((tlen - ppos, fmt))
return split_ans
@ -486,6 +489,7 @@ def create_formats(highlighter, add_css=True):
f.setFontWeight(QFont.Bold)
if add_css:
formats['css_sub_formats'] = create_css_formats(highlighter)
formats['spell'].setProperty(SPELL_PROPERTY, True)
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.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.syntax.base import SyntaxHighlighter
from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter
@ -427,7 +427,7 @@ class TextEdit(PlainTextEdit):
block = c.block()
while block.isValid():
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():
c.setPosition(block.position() + r.start)
c.setPosition(c.position() + r.length, c.KeepAnchor)
@ -568,26 +568,42 @@ class TextEdit(PlainTextEdit):
return False
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):
c = self.textCursor()
c.movePosition(c.Start)
block = c.block()
while block.isValid():
for r in block.layout().additionalFormats():
x = r.format.property(SPELL_PROPERTY).toPyObject()
if x is not None and word == x[0]:
if r.format.property(SPELL_PROPERTY).toBool() and self.text_for_range(block, r) == word:
self.highlighter.reformat_block(block)
break
block = block.next()
# Tooltips {{{
def syntax_format_for_cursor(self, cursor):
def syntax_range_for_cursor(self, cursor):
if cursor.isNull():
return
pos = cursor.positionInBlock()
for r in cursor.block().layout().additionalFormats():
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):
c = self.cursorForPosition(ev.pos())

View File

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