Make jumping to character locations robust against file edits

This commit is contained in:
Kovid Goyal 2015-01-22 10:25:30 +05:30
parent b05ee27ffe
commit a937ccdcdf
3 changed files with 48 additions and 16 deletions

View File

@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import posixpath, os import posixpath, os
from collections import namedtuple, defaultdict from collections import namedtuple, defaultdict, Counter
from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, OEB_FONTS from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, OEB_FONTS
from calibre.ebooks.oeb.polish.spell import get_all_words from calibre.ebooks.oeb.polish.spell import get_all_words
@ -93,19 +93,29 @@ def word_data(container, book_locale):
count, words = get_all_words(container, book_locale, get_word_count=True) count, words = get_all_words(container, book_locale, get_word_count=True)
return (count, tuple(Word(i, word, locale, v) for i, ((word, locale), v) in enumerate(words.iteritems()))) return (count, tuple(Word(i, word, locale, v) for i, ((word, locale), v) in enumerate(words.iteritems())))
Char = namedtuple('Char', 'id char codepoint usage') Char = namedtuple('Char', 'id char codepoint usage count')
def char_data(container): def char_data(container):
chars = defaultdict(list) chars = defaultdict(set)
counter = Counter()
def count(codepoint):
counter[codepoint] += 1
for name, is_linear in container.spine_names: for name, is_linear in container.spine_names:
if container.mime_map.get(name) not in OEB_DOCS: if container.mime_map.get(name) not in OEB_DOCS:
continue continue
raw = container.raw_data(name) raw = container.raw_data(name)
for i, codepoint in enumerate(ord_string(raw)): counts = Counter(ord_string(raw))
chars[codepoint].append(Location(name, character_offset=i)) counter.update(counts)
for codepoint in counts:
chars[codepoint].add(name)
nmap = {n:i for i, (n, l) in enumerate(container.spine_names)}
def sort_key(name):
return nmap.get(name, len(nmap)), numeric_sort_key(name)
for i, (codepoint, usage) in enumerate(chars.iteritems()): for i, (codepoint, usage) in enumerate(chars.iteritems()):
yield Char(i, safe_chr(codepoint), codepoint, sort_locations(container, usage)) yield Char(i, safe_chr(codepoint), codepoint, sorted(usage, key=sort_key), counter[codepoint])
def gather_data(container, book_locale): def gather_data(container, book_locale):
data = {'files':tuple(file_data(container))} data = {'files':tuple(file_data(container))}

View File

@ -1244,10 +1244,11 @@ class Boss(QObject):
self.gui.central.show_editor(editors[name]) self.gui.central.show_editor(editors[name])
editors[name].set_focus() editors[name].set_focus()
def edit_file_requested(self, name, syntax, mime): def edit_file_requested(self, name, syntax=None, mime=None):
if name in editors: if name in editors:
self.gui.central.show_editor(editors[name]) self.gui.central.show_editor(editors[name])
return editors[name] return editors[name]
mime = mime or current_container().mime_map.get(name, guess_type(name))
syntax = syntax or syntax_from_mime(name, mime) syntax = syntax or syntax_from_mime(name, mime)
if not syntax: if not syntax:
return error_dialog( return error_dialog(

View File

@ -20,7 +20,6 @@ from PyQt5.Qt import (
QStyledItemDelegate, QModelIndex, QRect, QStyle, QPalette, QTimer, QMenu) QStyledItemDelegate, QModelIndex, QRect, QStyle, QPalette, QTimer, QMenu)
from calibre import human_readable, fit_image from calibre import human_readable, fit_image
from calibre.ebooks.oeb.polish.container import guess_type
from calibre.ebooks.oeb.polish.report import gather_data from calibre.ebooks.oeb.polish.report import gather_data
from calibre.gui2 import error_dialog, question_dialog from calibre.gui2 import error_dialog, question_dialog
from calibre.gui2.tweak_book import current_container, tprefs, dictionaries from calibre.gui2.tweak_book import current_container, tprefs, dictionaries
@ -276,8 +275,7 @@ class Jump(object): # {{{
if boss is None: if boss is None:
return return
name = loc.name name = loc.name
mt = current_container().mime_map.get(name, guess_type(name)) editor = boss.edit_file_requested(name)
editor = boss.edit_file_requested(name, None, mt)
if editor is None: if editor is None:
return return
editor = editor.editor editor = editor.editor
@ -290,10 +288,6 @@ class Jump(object): # {{{
editor.setTextCursor(c) editor.setTextCursor(c)
if loc.text_on_line is not None: if loc.text_on_line is not None:
editor.find(regex.compile(regex.escape(loc.text_on_line))) editor.find(regex.compile(regex.escape(loc.text_on_line)))
elif loc.character_offset is not None:
c = editor.textCursor()
c.setPosition(loc.character_offset + 1) # put cursor after the character
editor.setTextCursor(c)
jump = Jump() # }}} jump = Jump() # }}}
@ -557,7 +551,7 @@ class CharsModel(FileCollection):
if col == 2: if col == 2:
return ('U+%04X' if entry.codepoint < 0x10000 else 'U+%06X') % entry.codepoint return ('U+%04X' if entry.codepoint < 0x10000 else 'U+%06X') % entry.codepoint
if col == 3: if col == 3:
return type('')(len(entry.usage)) return type('')(entry.count)
if role == Qt.UserRole: if role == Qt.UserRole:
try: try:
return self.files[index.row()] return self.files[index.row()]
@ -595,10 +589,37 @@ class CharsWidget(QWidget):
def double_clicked(self, index): def double_clicked(self, index):
entry = index.data(Qt.UserRole) entry = index.data(Qt.UserRole)
if entry is not None: if entry is not None:
jump((id(self), entry.id), entry.usage) self.find_next_location(entry)
def save(self): def save(self):
save_state('chars-table', bytearray(self.chars.horizontalHeader().saveState())) save_state('chars-table', bytearray(self.chars.horizontalHeader().saveState()))
def find_next_location(self, entry):
from calibre.gui2.tweak_book.boss import get_boss
boss = get_boss()
if boss is None:
return
files = entry.usage
current_editor_name = boss.currently_editing
if current_editor_name not in files:
current_editor_name = None
else:
idx = files.index(current_editor_name)
before, after = files[:idx], files[idx+1:]
files = [current_editor_name] + after + before + [current_editor_name]
pat = regex.compile(regex.escape(entry.char))
for file_name in files:
from_cursor = False
if file_name == current_editor_name:
from_cursor = True
current_editor_name = None
ed = boss.edit_file_requested(file_name)
if ed.editor.find(pat, complete=not from_cursor):
boss.show_editor(file_name)
return True
return False
# }}} # }}}
# Wrapper UI {{{ # Wrapper UI {{{