1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3 4__license__ = 'GPL v3' 5__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' 6 7from collections import defaultdict, deque 8 9from qt.core import QTextCursor, QTextBlockUserData, QTextLayout, QTimer 10 11from ..themes import highlight_to_char_format 12from calibre.gui2.tweak_book.widgets import BusyCursor 13from calibre.utils.icu import utf16_length 14from polyglot.builtins import iteritems 15 16 17def run_loop(user_data, state_map, formats, text): 18 state = user_data.state 19 i = 0 20 fix_offsets = utf16_length(text) != len(text) 21 seen_states = defaultdict(set) 22 while i < len(text): 23 orig_i = i 24 seen_states[i].add(state.parse) 25 fmt = state_map[state.parse](state, text, i, formats, user_data) 26 for num, f in fmt: 27 if num > 0: 28 if fix_offsets: 29 # We need to map offsets/lengths from UCS-4 to UTF-16 in 30 # which non-BMP characters are two code points wide 31 yield utf16_length(text[:i]), utf16_length(text[i:i+num]), f 32 else: 33 yield i, num, f 34 i += num 35 if orig_i == i and state.parse in seen_states[i]: 36 # Something went wrong in the syntax highlighter 37 print('Syntax highlighter returned a zero length format, parse state:', state.parse) 38 break 39 40 41class SimpleState: 42 43 __slots__ = ('parse',) 44 45 def __init__(self): 46 self.parse = 0 47 48 def copy(self): 49 s = SimpleState() 50 s.parse = self.parse 51 return s 52 53 54class SimpleUserData(QTextBlockUserData): 55 56 def __init__(self): 57 QTextBlockUserData.__init__(self) 58 self.state = SimpleState() 59 self.doc_name = None 60 61 def clear(self, state=None, doc_name=None): 62 self.state = SimpleState() if state is None else state 63 self.doc_name = doc_name 64 65 66class SyntaxHighlighter: 67 68 create_formats_func = lambda highlighter: {} 69 spell_attributes = () 70 tag_ok_for_spell = lambda x: False 71 user_data_factory = SimpleUserData 72 73 def __init__(self): 74 self.doc = None 75 self.doc_name = None 76 self.requests = deque() 77 self.ignore_requests = False 78 79 @property 80 def has_requests(self): 81 return bool(self.requests) 82 83 def apply_theme(self, theme): 84 self.theme = {k:highlight_to_char_format(v) for k, v in iteritems(theme)} 85 self.create_formats() 86 self.rehighlight() 87 88 def create_formats(self): 89 self.formats = self.create_formats_func() 90 91 def set_document(self, doc, doc_name=None): 92 old_doc = self.doc 93 if old_doc is not None: 94 old_doc.contentsChange.disconnect(self.reformat_blocks) 95 c = QTextCursor(old_doc) 96 c.beginEditBlock() 97 blk = old_doc.begin() 98 while blk.isValid(): 99 blk.layout().clearAdditionalFormats() 100 blk = blk.next() 101 c.endEditBlock() 102 self.doc = self.doc_name = None 103 if doc is not None: 104 self.doc = doc 105 self.doc_name = doc_name 106 doc.contentsChange.connect(self.reformat_blocks) 107 self.rehighlight() 108 109 def rehighlight(self): 110 doc = self.doc 111 if doc is None: 112 return 113 lb = doc.lastBlock() 114 with BusyCursor(): 115 self.reformat_blocks(0, 0, lb.position() + lb.length()) 116 117 def get_user_data(self, block): 118 ud = block.userData() 119 new_data = False 120 if ud is None: 121 ud = self.user_data_factory() 122 block.setUserData(ud) 123 new_data = True 124 return ud, new_data 125 126 def reformat_blocks(self, position, removed, added): 127 doc = self.doc 128 if doc is None or self.ignore_requests or not hasattr(self, 'state_map'): 129 return 130 131 block = doc.findBlock(position) 132 if not block.isValid(): 133 return 134 start_cursor = QTextCursor(block) 135 last_block = doc.findBlock(position + added + (1 if removed > 0 else 0)) 136 if not last_block.isValid(): 137 last_block = doc.lastBlock() 138 end_cursor = QTextCursor(last_block) 139 end_cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock) 140 self.requests.append((start_cursor, end_cursor)) 141 QTimer.singleShot(0, self.do_one_block) 142 143 def do_one_block(self): 144 try: 145 start_cursor, end_cursor = self.requests[0] 146 except IndexError: 147 return 148 self.ignore_requests = True 149 try: 150 block = start_cursor.block() 151 if not block.isValid(): 152 self.requests.popleft() 153 return 154 formats, force_next_highlight = self.parse_single_block(block) 155 self.apply_format_changes(block, formats) 156 try: 157 self.doc.markContentsDirty(block.position(), block.length()) 158 except AttributeError: 159 self.requests.clear() 160 return 161 ok = start_cursor.movePosition(QTextCursor.MoveOperation.NextBlock) 162 if not ok: 163 self.requests.popleft() 164 return 165 next_block = start_cursor.block() 166 if next_block.position() > end_cursor.position(): 167 if force_next_highlight: 168 end_cursor.setPosition(next_block.position() + 1) 169 else: 170 self.requests.popleft() 171 return 172 finally: 173 self.ignore_requests = False 174 QTimer.singleShot(0, self.do_one_block) 175 176 def join(self): 177 ''' Blocks until all pending highlighting requests are handled ''' 178 doc = self.doc 179 if doc is None: 180 self.requests.clear() 181 return 182 self.ignore_requests = True 183 try: 184 while self.requests: 185 start_cursor, end_cursor = self.requests.popleft() 186 block = start_cursor.block() 187 last_block = end_cursor.block() 188 if not last_block.isValid(): 189 last_block = doc.lastBlock() 190 end_pos = last_block.position() + last_block.length() 191 force_next_highlight = False 192 while block.isValid() and (force_next_highlight or block.position() < end_pos): 193 formats, force_next_highlight = self.parse_single_block(block) 194 self.apply_format_changes(block, formats) 195 doc.markContentsDirty(block.position(), block.length()) 196 block = block.next() 197 finally: 198 self.ignore_requests = False 199 200 @property 201 def is_working(self): 202 return bool(self.requests) 203 204 def parse_single_block(self, block): 205 ud, is_new_ud = self.get_user_data(block) 206 orig_state = ud.state 207 pblock = block.previous() 208 if pblock.isValid(): 209 start_state = pblock.userData() 210 if start_state is None: 211 start_state = self.user_data_factory().state 212 else: 213 start_state = start_state.state.copy() 214 else: 215 start_state = self.user_data_factory().state 216 ud.clear(state=start_state, doc_name=self.doc_name) # Ensure no stale user data lingers 217 formats = [] 218 for i, num, fmt in run_loop(ud, self.state_map, self.formats, str(block.text())): 219 if fmt is not None: 220 r = QTextLayout.FormatRange() 221 r.start, r.length, r.format = i, num, fmt 222 formats.append(r) 223 force_next_highlight = is_new_ud or ud.state != orig_state 224 return formats, force_next_highlight 225 226 def reformat_block(self, block): 227 if block.isValid(): 228 self.reformat_blocks(block.position(), 0, 1) 229 230 def apply_format_changes(self, block, formats): 231 layout = block.layout() 232 preedit_start = layout.preeditAreaPosition() 233 preedit_length = len(layout.preeditAreaText()) 234 if preedit_length != 0 and preedit_start != 0: 235 for r in formats: 236 # Adjust range by pre-edit text, if any 237 if r.start >= preedit_start: 238 r.start += preedit_length 239 elif r.start + r.length >= preedit_start: 240 r.length += preedit_length 241 layout.setAdditionalFormats(formats) 242