Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20140827092102.18574: * @file leoColorizer.py 

4#@@first 

5"""All colorizing code for Leo.""" 

6 

7# Indicated code are copyright (c) Jupyter Development Team. 

8# Distributed under the terms of the Modified BSD License. 

9 

10#@+<< imports >> 

11#@+node:ekr.20140827092102.18575: ** << imports >> (leoColorizer.py) 

12import re 

13import string 

14import time 

15from typing import Any, Callable, Dict, List, Tuple 

16# 

17# Third-part tools. 

18try: 

19 import pygments # type:ignore 

20except ImportError: 

21 pygments = None # type:ignore 

22# 

23# Leo imports... 

24from leo.core import leoGlobals as g 

25 

26from leo.core.leoColor import leo_color_database 

27# 

28# Qt imports. May fail from the bridge. 

29try: # #1973 

30 from leo.core.leoQt import Qsci, QtGui, QtWidgets 

31 from leo.core.leoQt import UnderlineStyle, Weight # #2330 

32except Exception: 

33 Qsci = QtGui = QtWidgets = None 

34 UnderlineStyle = Weight = None 

35#@-<< imports >> 

36#@+others 

37#@+node:ekr.20190323044524.1: ** function: make_colorizer 

38def make_colorizer(c, widget, wrapper): 

39 """Return an instance of JEditColorizer or PygmentsColorizer.""" 

40 use_pygments = pygments and c.config.getBool('use-pygments', default=False) 

41 if use_pygments: 

42 return PygmentsColorizer(c, widget, wrapper) 

43 return JEditColorizer(c, widget, wrapper) 

44#@+node:ekr.20170127141855.1: ** class BaseColorizer 

45class BaseColorizer: 

46 """The base class for all Leo colorizers.""" 

47 #@+others 

48 #@+node:ekr.20190324044744.1: *3* bc.__init__ 

49 def __init__(self, c, widget=None, wrapper=None): 

50 """ctor for BaseColorizer class.""" 

51 # 

52 # Copy args... 

53 self.c = c 

54 self.widget = widget 

55 if widget: 

56 # #503: widget may be None during unit tests. 

57 widget.leo_colorizer = self 

58 self.wrapper = wrapper 

59 # This assert is not true when using multiple body editors 

60 # assert(wrapper == self.c.frame.body.wrapper) 

61 # 

62 # Common state ivars... 

63 self.enabled = False 

64 # Per-node enable/disable flag. 

65 # Set by updateSyntaxColorer. 

66 self.highlighter = g.NullObject() 

67 # May be overridden in subclass... 

68 self.language = 'python' 

69 # set by scanLanguageDirectives. 

70 self.showInvisibles = False 

71 # 

72 # Statistics.... 

73 self.count = 0 

74 self.full_recolor_count = 0 

75 # For unit tests. 

76 self.recolorCount = 0 

77 # 

78 # For traces... 

79 self.matcher_name = '' 

80 self.delegate_name = '' 

81 #@+node:ekr.20190324045134.1: *3* bc.init 

82 def init(self, p): 

83 """May be over-ridden in subclasses.""" 

84 pass 

85 #@+node:ekr.20170127142001.1: *3* bc.updateSyntaxColorer & helpers 

86 # Note: these are used by unit tests. 

87 

88 def updateSyntaxColorer(self, p): 

89 """ 

90 Scan for color directives in p and its ancestors. 

91 Return True unless an coloring is unambiguously disabled. 

92 Called from Leo's node-selection logic and from the colorizer. 

93 """ 

94 if p: # This guard is required. 

95 try: 

96 self.enabled = self.useSyntaxColoring(p) 

97 self.language = self.scanLanguageDirectives(p) 

98 except Exception: 

99 g.es_print('unexpected exception in updateSyntaxColorer') 

100 g.es_exception() 

101 #@+node:ekr.20170127142001.2: *4* bjc.scanLanguageDirectives 

102 def scanLanguageDirectives(self, p): 

103 """Return language based on the directives in p's ancestors.""" 

104 c = self.c 

105 language = g.getLanguageFromAncestorAtFileNode(p) 

106 return language or c.target_language 

107 #@+node:ekr.20170127142001.7: *4* bjc.useSyntaxColoring & helper 

108 def useSyntaxColoring(self, p): 

109 """True if p's parents enable coloring in p.""" 

110 # Special cases for the selected node. 

111 d = self.findColorDirectives(p) 

112 if 'killcolor' in d: 

113 return False 

114 if 'nocolor-node' in d: 

115 return False 

116 # Now look at the parents. 

117 for p in p.parents(): 

118 d = self.findColorDirectives(p) 

119 # @killcolor anywhere disables coloring. 

120 if 'killcolor' in d: 

121 return False 

122 # unambiguous @color enables coloring. 

123 if 'color' in d and 'nocolor' not in d: 

124 return True 

125 # Unambiguous @nocolor disables coloring. 

126 if 'nocolor' in d and 'color' not in d: 

127 return False 

128 return True 

129 #@+node:ekr.20170127142001.8: *5* bjc.findColorDirectives 

130 # Order is important: put longest matches first. 

131 color_directives_pat = re.compile( 

132 r'(^@color|^@killcolor|^@nocolor-node|^@nocolor)' 

133 , re.MULTILINE) 

134 

135 def findColorDirectives(self, p): 

136 """Return a dict with each color directive in p.b, without the leading '@'.""" 

137 d = {} 

138 for m in self.color_directives_pat.finditer(p.b): 

139 word = m.group(0)[1:] 

140 d[word] = word 

141 return d 

142 #@-others 

143#@+node:ekr.20190324115354.1: ** class BaseJEditColorizer (BaseColorizer) 

144class BaseJEditColorizer(BaseColorizer): 

145 """A class containing common JEdit tags machinery.""" 

146 # No need for a ctor. 

147 #@+others 

148 #@+node:ekr.20110605121601.18576: *3* bjc.addImportedRules 

149 def addImportedRules(self, mode, rulesDict, rulesetName): 

150 """Append any imported rules at the end of the rulesets specified in mode.importDict""" 

151 if self.importedRulesets.get(rulesetName): 

152 return 

153 self.importedRulesets[rulesetName] = True 

154 names = mode.importDict.get( 

155 rulesetName, []) if hasattr(mode, 'importDict') else [] 

156 for name in names: 

157 savedBunch = self.modeBunch 

158 ok = self.init_mode(name) 

159 if ok: 

160 rulesDict2 = self.rulesDict 

161 for key in rulesDict2.keys(): 

162 aList = self.rulesDict.get(key, []) 

163 aList2 = rulesDict2.get(key) 

164 if aList2: 

165 # Don't add the standard rules again. 

166 rules = [z for z in aList2 if z not in aList] 

167 if rules: 

168 aList.extend(rules) 

169 self.rulesDict[key] = aList 

170 self.initModeFromBunch(savedBunch) 

171 #@+node:ekr.20110605121601.18577: *3* bjc.addLeoRules 

172 def addLeoRules(self, theDict): 

173 """Put Leo-specific rules to theList.""" 

174 # pylint: disable=no-member 

175 table = [ 

176 # Rules added at front are added in **reverse** order. 

177 ('@', self.match_leo_keywords, True), # Called after all other Leo matchers. 

178 # Debatable: Leo keywords override langauge keywords. 

179 ('@', self.match_at_color, True), 

180 ('@', self.match_at_killcolor, True), 

181 ('@', self.match_at_language, True), # 2011/01/17 

182 ('@', self.match_at_nocolor, True), 

183 ('@', self.match_at_nocolor_node, True), 

184 ('@', self.match_at_wrap, True), # 2015/06/22 

185 ('@', self.match_doc_part, True), 

186 ('f', self.match_url_f, True), 

187 ('g', self.match_url_g, True), 

188 ('h', self.match_url_h, True), 

189 ('m', self.match_url_m, True), 

190 ('n', self.match_url_n, True), 

191 ('p', self.match_url_p, True), 

192 ('t', self.match_url_t, True), 

193 ('u', self.match_unl, True), 

194 ('w', self.match_url_w, True), 

195 # ('<', self.match_image, True), 

196 ('<', self.match_section_ref, True), # Called **first**. 

197 # Rules added at back are added in normal order. 

198 (' ', self.match_blanks, False), 

199 ('\t', self.match_tabs, False), 

200 ] 

201 if self.c.config.getBool("color-trailing-whitespace"): 

202 table += [ 

203 (' ', self.match_trailing_ws, True), 

204 ('\t', self.match_trailing_ws, True), 

205 ] 

206 for ch, rule, atFront, in table: 

207 # Replace the bound method by an unbound method. 

208 rule = rule.__func__ 

209 theList = theDict.get(ch, []) 

210 if rule not in theList: 

211 if atFront: 

212 theList.insert(0, rule) 

213 else: 

214 theList.append(rule) 

215 theDict[ch] = theList 

216 #@+node:ekr.20111024091133.16702: *3* bjc.configure_hard_tab_width 

217 def configure_hard_tab_width(self, font): 

218 """ 

219 Set the width of a hard tab. 

220 

221 Qt does not appear to have the required methods. Indeed, 

222 https://stackoverflow.com/questions/13027091/how-to-override-tab-width-in-qt 

223 assumes that QTextEdit's have only a single font(!). 

224 

225 This method probabably only works probably if the body text contains 

226 a single @language directive, and it may not work properly even then. 

227 """ 

228 c, widget = self.c, self.widget 

229 if isinstance(widget, QtWidgets.QTextEdit): 

230 # #1919: https://forum.qt.io/topic/99371/how-to-set-tab-stop-width-and-space-width 

231 fm = QtGui.QFontMetrics(font) 

232 try: # fm.horizontalAdvance 

233 width = fm.horizontalAdvance(' ') * abs(c.tab_width) 

234 widget.setTabStopDistance(width) 

235 except Exception: 

236 width = fm.width(' ') * abs(c.tab_width) 

237 widget.setTabStopWidth(width) # Obsolete. 

238 else: 

239 # To do: configure the QScintilla widget. 

240 pass 

241 #@+node:ekr.20110605121601.18578: *3* bjc.configure_tags & helpers 

242 def configure_tags(self): 

243 """Configure all tags.""" 

244 wrapper = self.wrapper 

245 if wrapper and hasattr(wrapper, 'start_tag_configure'): 

246 wrapper.start_tag_configure() 

247 self.configure_fonts() 

248 self.configure_colors() 

249 self.configure_variable_tags() 

250 if wrapper and hasattr(wrapper, 'end_tag_configure'): 

251 wrapper.end_tag_configure() 

252 #@+node:ekr.20190324172632.1: *4* bjc.configure_colors 

253 def configure_colors(self): 

254 """Configure all colors in the default colors dict.""" 

255 c, wrapper = self.c, self.wrapper 

256 getColor = c.config.getColor 

257 # getColor puts the color name in standard form: 

258 # color = color.replace(' ', '').lower().strip() 

259 for key in sorted(self.default_colors_dict.keys()): 

260 option_name, default_color = self.default_colors_dict[key] 

261 color = ( 

262 getColor(f"{self.language}_{option_name}") or 

263 getColor(option_name) or 

264 default_color 

265 ) 

266 # Must use foreground, not fg. 

267 try: 

268 wrapper.tag_configure(key, foreground=color) 

269 except Exception: # Recover after a user settings error. 

270 g.es_exception() 

271 wrapper.tag_configure(key, foreground=default_color) 

272 #@+node:ekr.20190324172242.1: *4* bjc.configure_fonts & helper 

273 def configure_fonts(self): 

274 """Configure all fonts in the default fonts dict.""" 

275 c = self.c 

276 isQt = g.app.gui.guiName().startswith('qt') 

277 wrapper = self.wrapper 

278 # 

279 # Get the default body font. 

280 defaultBodyfont = self.fonts.get('default_body_font') 

281 if not defaultBodyfont: 

282 defaultBodyfont = c.config.getFontFromParams( 

283 "body_text_font_family", "body_text_font_size", 

284 "body_text_font_slant", "body_text_font_weight", 

285 c.config.defaultBodyFontSize) 

286 self.fonts['default_body_font'] = defaultBodyfont 

287 # 

288 # Set all fonts. 

289 for key in sorted(self.default_font_dict.keys()): 

290 option_name = self.default_font_dict[key] 

291 # Find language specific setting before general setting. 

292 table = ( 

293 f"{self.language}_{option_name}", 

294 option_name, 

295 ) 

296 for name in table: 

297 font = self.fonts.get(name) 

298 if font: 

299 break 

300 font = self.find_font(key, name) 

301 if font: 

302 self.fonts[key] = font 

303 wrapper.tag_configure(key, font=font) 

304 if isQt and key == 'url': 

305 font.setUnderline(True) 

306 # #1919: This really isn't correct. 

307 self.configure_hard_tab_width(font) 

308 break 

309 else: 

310 # Neither setting exists. 

311 self.fonts[key] = None # Essential 

312 wrapper.tag_configure(key, font=defaultBodyfont) 

313 #@+node:ekr.20190326034006.1: *5* bjc.find_font 

314 zoom_dict: Dict[str, int] = {} 

315 # Keys are key::settings_names, values are cumulative font size. 

316 

317 def find_font(self, key, setting_name): 

318 """ 

319 Return the font for the given setting name. 

320 """ 

321 trace = 'zoom' in g.app.debug 

322 c, get = self.c, self.c.config.get 

323 default_size = c.config.defaultBodyFontSize 

324 for name in (setting_name, setting_name.rstrip('_font')): 

325 size_error = False 

326 family = get(name + '_family', 'family') 

327 size = get(name + '_size', 'size') 

328 slant = get(name + '_slant', 'slant') 

329 weight = get(name + '_weight', 'weight') 

330 if family or slant or weight or size: 

331 family = family or g.app.config.defaultFontFamily 

332 key = f"{key}::{setting_name}" 

333 if key in self.zoom_dict: 

334 old_size = self.zoom_dict.get(key) 

335 else: 

336 # It's a good idea to set size explicitly. 

337 old_size = size or default_size 

338 if isinstance(old_size, str): 

339 # All settings should be in units of points. 

340 try: 

341 if old_size.endswith(('pt', 'px'),): 

342 old_size = int(old_size[:-2]) 

343 else: 

344 old_size = int(old_size) 

345 except ValueError: 

346 size_error = True 

347 elif not isinstance(old_size, int): 

348 size_error = True 

349 if size_error: 

350 g.trace('bad old_size:', old_size.__class__, old_size) 

351 size = old_size 

352 else: 

353 # #490: Use c.zoom_size if it exists. 

354 zoom_delta = getattr(c, 'zoom_delta', 0) 

355 if zoom_delta: 

356 size = old_size + zoom_delta 

357 self.zoom_dict[key] = size 

358 slant = slant or 'roman' 

359 weight = weight or 'normal' 

360 size = str(size) 

361 font = g.app.gui.getFontFromParams(family, size, slant, weight) 

362 # A good trace: the key shows what is happening. 

363 if font: 

364 if trace: 

365 g.trace( 

366 f"key: {key:>35} family: {family or 'None'} " 

367 f"size: {size or 'None'} {slant} {weight}") 

368 return font 

369 return None 

370 #@+node:ekr.20110605121601.18579: *4* bjc.configure_variable_tags 

371 def configure_variable_tags(self): 

372 c = self.c 

373 wrapper = self.wrapper 

374 wrapper.tag_configure("link", underline=0) 

375 use_pygments = pygments and c.config.getBool('use-pygments', default=False) 

376 name = 'name.other' if use_pygments else 'name' 

377 wrapper.tag_configure(name, underline=1 if self.underline_undefined else 0) 

378 for name, option_name, default_color in ( 

379 # ("blank", "show_invisibles_space_background_color", "Gray90"), 

380 # ("tab", "show_invisibles_tab_background_color", "Gray80"), 

381 ("elide", None, "yellow"), 

382 ): 

383 if self.showInvisibles: 

384 color = c.config.getColor(option_name) if option_name else default_color 

385 else: 

386 option_name, default_color = self.default_colors_dict.get( 

387 name, (None, None),) 

388 color = c.config.getColor(option_name) if option_name else '' 

389 try: 

390 wrapper.tag_configure(name, background=color) 

391 except Exception: # A user error. 

392 wrapper.tag_configure(name, background=default_color) 

393 g.es_print(f"invalid setting: {name!r} = {default_color!r}") 

394 # Special case: 

395 if not self.showInvisibles: 

396 wrapper.tag_configure("elide", elide="1") 

397 #@+node:ekr.20110605121601.18574: *3* bjc.defineDefaultColorsDict 

398 #@@nobeautify 

399 

400 def defineDefaultColorsDict (self): 

401 

402 # These defaults are sure to exist. 

403 self.default_colors_dict = { 

404 # 

405 # Used in Leo rules... 

406 # tag name :( option name, default color), 

407 'blank' :('show_invisibles_space_color', '#E5E5E5'), # gray90 

408 'docpart' :('doc_part_color', 'red'), 

409 'leokeyword' :('leo_keyword_color', 'blue'), 

410 'link' :('section_name_color', 'red'), 

411 'name' :('undefined_section_name_color','red'), 

412 'namebrackets' :('section_name_brackets_color', 'blue'), 

413 'tab' :('show_invisibles_tab_color', '#CCCCCC'), # gray80 

414 'url' :('url_color', 'purple'), 

415 # 

416 # Pygments tags. Non-default values are taken from 'default' style. 

417 # 

418 # Top-level... 

419 # tag name :( option name, default color), 

420 'error' :('error', '#FF0000'), # border 

421 'other' :('other', 'white'), 

422 'punctuation' :('punctuation', 'white'), 

423 'whitespace' :('whitespace', '#bbbbbb'), 

424 'xt' :('xt', '#bbbbbb'), 

425 # 

426 # Comment... 

427 # tag name :( option name, default color), 

428 'comment' :('comment', '#408080'), # italic 

429 'comment.hashbang' :('comment.hashbang', '#408080'), 

430 'comment.multiline' :('comment.multiline', '#408080'), 

431 'comment.special' :('comment.special', '#408080'), 

432 'comment.preproc' :('comment.preproc', '#BC7A00'), # noitalic 

433 'comment.single' :('comment.single', '#BC7A00'), # italic 

434 # 

435 # Generic... 

436 # tag name :( option name, default color), 

437 'generic' :('generic', '#A00000'), 

438 'generic.deleted' :('generic.deleted', '#A00000'), 

439 'generic.emph' :('generic.emph', '#000080'), # italic 

440 'generic.error' :('generic.error', '#FF0000'), 

441 'generic.heading' :('generic.heading', '#000080'), # bold 

442 'generic.inserted' :('generic.inserted', '#00A000'), 

443 'generic.output' :('generic.output', '#888'), 

444 'generic.prompt' :('generic.prompt', '#000080'), # bold 

445 'generic.strong' :('generic.strong', '#000080'), # bold 

446 'generic.subheading':('generic.subheading', '#800080'), # bold 

447 'generic.traceback' :('generic.traceback', '#04D'), 

448 # 

449 # Keyword... 

450 # tag name :( option name, default color), 

451 'keyword' :('keyword', '#008000'), # bold 

452 'keyword.constant' :('keyword.constant', '#008000'), 

453 'keyword.declaration' :('keyword.declaration', '#008000'), 

454 'keyword.namespace' :('keyword.namespace', '#008000'), 

455 'keyword.pseudo' :('keyword.pseudo', '#008000'), # nobold 

456 'keyword.reserved' :('keyword.reserved', '#008000'), 

457 'keyword.type' :('keyword.type', '#B00040'), 

458 # 

459 # Literal... 

460 # tag name :( option name, default color), 

461 'literal' :('literal', 'white'), 

462 'literal.date' :('literal.date', 'white'), 

463 # 

464 # Name... 

465 # tag name :( option name, default color 

466 # 'name' defined below. 

467 'name.attribute' :('name.attribute', '#7D9029'), # bold 

468 'name.builtin' :('name.builtin', '#008000'), 

469 'name.builtin.pseudo' :('name.builtin.pseudo','#008000'), 

470 'name.class' :('name.class', '#0000FF'), # bold 

471 'name.constant' :('name.constant', '#880000'), 

472 'name.decorator' :('name.decorator', '#AA22FF'), 

473 'name.entity' :('name.entity', '#999999'), # bold 

474 'name.exception' :('name.exception', '#D2413A'), # bold 

475 'name.function' :('name.function', '#0000FF'), 

476 'name.function.magic' :('name.function.magic','#0000FF'), 

477 'name.label' :('name.label', '#A0A000'), 

478 'name.namespace' :('name.namespace', '#0000FF'), # bold 

479 'name.other' :('name.other', 'red'), 

480 'name.pygments' :('name.pygments', 'white'), 

481 # A hack: getLegacyFormat returns name.pygments instead of name. 

482 'name.tag' :('name.tag', '#008000'), # bold 

483 'name.variable' :('name.variable', '#19177C'), 

484 'name.variable.class' :('name.variable.class', '#19177C'), 

485 'name.variable.global' :('name.variable.global', '#19177C'), 

486 'name.variable.instance':('name.variable.instance', '#19177C'), 

487 'name.variable.magic' :('name.variable.magic', '#19177C'), 

488 # 

489 # Number... 

490 # tag name :( option name, default color 

491 'number' :('number', '#666666'), 

492 'number.bin' :('number.bin', '#666666'), 

493 'number.float' :('number.float', '#666666'), 

494 'number.hex' :('number.hex', '#666666'), 

495 'number.integer' :('number.integer', '#666666'), 

496 'number.integer.long' :('number.integer.long','#666666'), 

497 'number.oct' :('number.oct', '#666666'), 

498 # 

499 # Operator... 

500 # tag name :( option name, default color 

501 # 'operator' defined below. 

502 'operator.word' :('operator.Word', '#AA22FF'), # bold 

503 # 

504 # String... 

505 # tag name :( option name, default color 

506 'string' :('string', '#BA2121'), 

507 'string.affix' :('string.affix', '#BA2121'), 

508 'string.backtick' :('string.backtick', '#BA2121'), 

509 'string.char' :('string.char', '#BA2121'), 

510 'string.delimiter' :('string.delimiter', '#BA2121'), 

511 'string.doc' :('string.doc', '#BA2121'), # italic 

512 'string.double' :('string.double', '#BA2121'), 

513 'string.escape' :('string.escape', '#BB6622'), # bold 

514 'string.heredoc' :('string.heredoc', '#BA2121'), 

515 'string.interpol' :('string.interpol', '#BB6688'), # bold 

516 'string.other' :('string.other', '#008000'), 

517 'string.regex' :('string.regex', '#BB6688'), 

518 'string.single' :('string.single', '#BA2121'), 

519 'string.symbol' :('string.symbol', '#19177C'), 

520 # 

521 # jEdit tags. 

522 # tag name :( option name, default color), 

523 'comment1' :('comment1_color', 'red'), 

524 'comment2' :('comment2_color', 'red'), 

525 'comment3' :('comment3_color', 'red'), 

526 'comment4' :('comment4_color', 'red'), 

527 'function' :('function_color', 'black'), 

528 'keyword1' :('keyword1_color', 'blue'), 

529 'keyword2' :('keyword2_color', 'blue'), 

530 'keyword3' :('keyword3_color', 'blue'), 

531 'keyword4' :('keyword4_color', 'blue'), 

532 'keyword5' :('keyword5_color', 'blue'), 

533 'label' :('label_color', 'black'), 

534 'literal1' :('literal1_color', '#00aa00'), 

535 'literal2' :('literal2_color', '#00aa00'), 

536 'literal3' :('literal3_color', '#00aa00'), 

537 'literal4' :('literal4_color', '#00aa00'), 

538 'markup' :('markup_color', 'red'), 

539 'null' :('null_color', None), #'black'), 

540 'operator' :('operator_color', 'black'), 

541 'trailing_whitespace': ('trailing_whitespace_color', '#808080'), 

542 } 

543 #@+node:ekr.20110605121601.18575: *3* bjc.defineDefaultFontDict 

544 #@@nobeautify 

545 

546 def defineDefaultFontDict (self): 

547 

548 self.default_font_dict = { 

549 # 

550 # Used in Leo rules... 

551 # tag name : option name 

552 'blank' :'show_invisibles_space_font', # 2011/10/24. 

553 'docpart' :'doc_part_font', 

554 'leokeyword' :'leo_keyword_font', 

555 'link' :'section_name_font', 

556 'name' :'undefined_section_name_font', 

557 'namebrackets' :'section_name_brackets_font', 

558 'tab' :'show_invisibles_tab_font', # 2011/10/24. 

559 'url' :'url_font', 

560 # 

561 # Pygments tags (lower case)... 

562 # tag name : option name 

563 "comment" :'comment1_font', 

564 "comment.preproc" :'comment2_font', 

565 "comment.single" :'comment1_font', 

566 "error" :'null_font', 

567 "generic.deleted" :'literal4_font', 

568 "generic.emph" :'literal4_font', 

569 "generic.error" :'literal4_font', 

570 "generic.heading" :'literal4_font', 

571 "generic.inserted" :'literal4_font', 

572 "generic.output" :'literal4_font', 

573 "generic.prompt" :'literal4_font', 

574 "generic.strong" :'literal4_font', 

575 "generic.subheading":'literal4_font', 

576 "generic.traceback" :'literal4_font', 

577 "keyword" :'keyword1_font', 

578 "keyword.pseudo" :'keyword2_font', 

579 "keyword.type" :'keyword3_font', 

580 "name.attribute" :'null_font', 

581 "name.builtin" :'null_font', 

582 "name.class" :'null_font', 

583 "name.constant" :'null_font', 

584 "name.decorator" :'null_font', 

585 "name.entity" :'null_font', 

586 "name.exception" :'null_font', 

587 "name.function" :'null_font', 

588 "name.label" :'null_font', 

589 "name.namespace" :'null_font', 

590 "name.tag" :'null_font', 

591 "name.variable" :'null_font', 

592 "number" :'null_font', 

593 "operator.word" :'keyword4_font', 

594 "string" :'literal1_font', 

595 "string.doc" :'literal1_font', 

596 "string.escape" :'literal1_font', 

597 "string.interpol" :'literal1_font', 

598 "string.other" :'literal1_font', 

599 "string.regex" :'literal1_font', 

600 "string.single" :'literal1_font', 

601 "string.symbol" :'literal1_font', 

602 'xt' :'text_font', 

603 "whitespace" :'text_font', 

604 # 

605 # jEdit tags. 

606 # tag name : option name 

607 'comment1' :'comment1_font', 

608 'comment2' :'comment2_font', 

609 'comment3' :'comment3_font', 

610 'comment4' :'comment4_font', 

611 #'default' :'default_font', 

612 'function' :'function_font', 

613 'keyword1' :'keyword1_font', 

614 'keyword2' :'keyword2_font', 

615 'keyword3' :'keyword3_font', 

616 'keyword4' :'keyword4_font', 

617 'keyword5' :'keyword5_font', 

618 'label' :'label_font', 

619 'literal1' :'literal1_font', 

620 'literal2' :'literal2_font', 

621 'literal3' :'literal3_font', 

622 'literal4' :'literal4_font', 

623 'markup' :'markup_font', 

624 # 'nocolor' This tag is used, but never generates code. 

625 'null' :'null_font', 

626 'operator' :'operator_font', 

627 'trailing_whitespace' :'trailing_whitespace_font', 

628 } 

629 #@+node:ekr.20110605121601.18573: *3* bjc.defineLeoKeywordsDict 

630 def defineLeoKeywordsDict(self): 

631 self.leoKeywordsDict = {} 

632 for key in g.globalDirectiveList: 

633 self.leoKeywordsDict[key] = 'leokeyword' 

634 #@+node:ekr.20170514054524.1: *3* bjc.getFontFromParams 

635 def getFontFromParams(self, family, size, slant, weight, defaultSize=12): 

636 return None 

637 

638 # def setFontFromConfig(self): 

639 # pass 

640 #@+node:ekr.20110605121601.18581: *3* bjc.init_mode & helpers 

641 def init_mode(self, name): 

642 """Name may be a language name or a delegate name.""" 

643 if not name: 

644 return False 

645 if name == 'latex': 

646 name = 'tex' 

647 # #1088: use tex mode for both tex and latex. 

648 language, rulesetName = self.nameToRulesetName(name) 

649 if 'coloring' in g.app.debug and not g.unitTesting: 

650 print(f"language: {language!r}, rulesetName: {rulesetName!r}") 

651 bunch = self.modes.get(rulesetName) 

652 if bunch: 

653 if bunch.language == 'unknown-language': 

654 return False 

655 self.initModeFromBunch(bunch) 

656 self.language = language # 2011/05/30 

657 return True 

658 # Don't try to import a non-existent language. 

659 path = g.os_path_join(g.app.loadDir, '..', 'modes') 

660 fn = g.os_path_join(path, f"{language}.py") 

661 if g.os_path_exists(fn): 

662 mode = g.import_module(name=f"leo.modes.{language}") 

663 else: 

664 mode = None 

665 return self.init_mode_from_module(name, mode) 

666 #@+node:btheado.20131124162237.16303: *4* bjc.init_mode_from_module 

667 def init_mode_from_module(self, name, mode): 

668 """ 

669 Name may be a language name or a delegate name. 

670 Mode is a python module or class containing all 

671 coloring rule attributes for the mode. 

672 """ 

673 language, rulesetName = self.nameToRulesetName(name) 

674 if mode: 

675 # A hack to give modes/forth.py access to c. 

676 if hasattr(mode, 'pre_init_mode'): 

677 mode.pre_init_mode(self.c) 

678 else: 

679 # Create a dummy bunch to limit recursion. 

680 self.modes[rulesetName] = self.modeBunch = g.Bunch( 

681 attributesDict={}, 

682 defaultColor=None, 

683 keywordsDict={}, 

684 language='unknown-language', 

685 mode=mode, 

686 properties={}, 

687 rulesDict={}, 

688 rulesetName=rulesetName, 

689 word_chars=self.word_chars, # 2011/05/21 

690 ) 

691 self.rulesetName = rulesetName 

692 self.language = 'unknown-language' 

693 return False 

694 self.language = language 

695 self.rulesetName = rulesetName 

696 self.properties = getattr(mode, 'properties', None) or {} 

697 # 

698 # #1334: Careful: getattr(mode, ivar, {}) might be None! 

699 # 

700 d: Dict[Any, Any] = getattr(mode, 'keywordsDictDict', {}) or {} 

701 self.keywordsDict = d.get(rulesetName, {}) 

702 self.setKeywords() 

703 d = getattr(mode, 'attributesDictDict', {}) or {} 

704 self.attributesDict: Dict[str, Any] = d.get(rulesetName, {}) 

705 self.setModeAttributes() 

706 d = getattr(mode, 'rulesDictDict', {}) or {} 

707 self.rulesDict: Dict[str, Any] = d.get(rulesetName, {}) 

708 self.addLeoRules(self.rulesDict) 

709 self.defaultColor = 'null' 

710 self.mode = mode 

711 self.modes[rulesetName] = self.modeBunch = g.Bunch( 

712 attributesDict=self.attributesDict, 

713 defaultColor=self.defaultColor, 

714 keywordsDict=self.keywordsDict, 

715 language=self.language, 

716 mode=self.mode, 

717 properties=self.properties, 

718 rulesDict=self.rulesDict, 

719 rulesetName=self.rulesetName, 

720 word_chars=self.word_chars, # 2011/05/21 

721 ) 

722 # Do this after 'officially' initing the mode, to limit recursion. 

723 self.addImportedRules(mode, self.rulesDict, rulesetName) 

724 self.updateDelimsTables() 

725 initialDelegate = self.properties.get('initialModeDelegate') 

726 if initialDelegate: 

727 # Replace the original mode by the delegate mode. 

728 self.init_mode(initialDelegate) 

729 language2, rulesetName2 = self.nameToRulesetName(initialDelegate) 

730 self.modes[rulesetName] = self.modes.get(rulesetName2) 

731 self.language = language2 # 2017/01/31 

732 else: 

733 self.language = language # 2017/01/31 

734 return True 

735 #@+node:ekr.20110605121601.18582: *4* bjc.nameToRulesetName 

736 def nameToRulesetName(self, name): 

737 """ 

738 Compute language and rulesetName from name, which is either a language 

739 name or a delegate name. 

740 """ 

741 if not name: 

742 return '' 

743 name = name.lower() 

744 # #1334. Lower-case the name, regardless of the spelling in @language. 

745 i = name.find('::') 

746 if i == -1: 

747 language = name 

748 # New in Leo 5.0: allow delegated language names. 

749 language = g.app.delegate_language_dict.get(language, language) 

750 rulesetName = f"{language}_main" 

751 else: 

752 language = name[:i] 

753 delegate = name[i + 2 :] 

754 rulesetName = self.munge(f"{language}_{delegate}") 

755 return language, rulesetName 

756 #@+node:ekr.20110605121601.18583: *4* bjc.setKeywords 

757 def setKeywords(self): 

758 """ 

759 Initialize the keywords for the present language. 

760 

761 Set self.word_chars ivar to string.letters + string.digits 

762 plus any other character appearing in any keyword. 

763 """ 

764 # Add any new user keywords to leoKeywordsDict. 

765 d = self.keywordsDict 

766 keys = list(d.keys()) 

767 for s in g.globalDirectiveList: 

768 key = '@' + s 

769 if key not in keys: 

770 d[key] = 'leokeyword' 

771 # Create a temporary chars list. It will be converted to a dict later. 

772 chars = [z for z in string.ascii_letters + string.digits] 

773 for key in list(d.keys()): 

774 for ch in key: 

775 if ch not in chars: 

776 chars.append(g.checkUnicode(ch)) 

777 # jEdit2Py now does this check, so this isn't really needed. 

778 # But it is needed for forth.py. 

779 for ch in (' ', '\t'): 

780 if ch in chars: 

781 # g.es_print('removing %s from word_chars' % (repr(ch))) 

782 chars.remove(ch) 

783 # Convert chars to a dict for faster access. 

784 self.word_chars: Dict[str, str] = {} 

785 for z in chars: 

786 self.word_chars[z] = z 

787 #@+node:ekr.20110605121601.18584: *4* bjc.setModeAttributes 

788 def setModeAttributes(self): 

789 """ 

790 Set the ivars from self.attributesDict, 

791 converting 'true'/'false' to True and False. 

792 """ 

793 d = self.attributesDict 

794 aList = ( 

795 ('default', 'null'), 

796 ('digit_re', ''), 

797 ('escape', ''), # New in Leo 4.4.2. 

798 ('highlight_digits', True), 

799 ('ignore_case', True), 

800 ('no_word_sep', ''), 

801 ) 

802 for key, default in aList: 

803 val = d.get(key, default) 

804 if val in ('true', 'True'): 

805 val = True 

806 if val in ('false', 'False'): 

807 val = False 

808 setattr(self, key, val) 

809 #@+node:ekr.20110605121601.18585: *4* bjc.initModeFromBunch 

810 def initModeFromBunch(self, bunch): 

811 self.modeBunch = bunch 

812 self.attributesDict = bunch.attributesDict 

813 self.setModeAttributes() 

814 self.defaultColor = bunch.defaultColor 

815 self.keywordsDict = bunch.keywordsDict 

816 self.language = bunch.language 

817 self.mode = bunch.mode 

818 self.properties = bunch.properties 

819 self.rulesDict = bunch.rulesDict 

820 self.rulesetName = bunch.rulesetName 

821 self.word_chars = bunch.word_chars # 2011/05/21 

822 #@+node:ekr.20110605121601.18586: *4* bjc.updateDelimsTables 

823 def updateDelimsTables(self): 

824 """Update g.app.language_delims_dict if no entry for the language exists.""" 

825 d = self.properties 

826 lineComment = d.get('lineComment') 

827 startComment = d.get('commentStart') 

828 endComment = d.get('commentEnd') 

829 if lineComment and startComment and endComment: 

830 delims = f"{lineComment} {startComment} {endComment}" 

831 elif startComment and endComment: 

832 delims = f"{startComment} {endComment}" 

833 elif lineComment: 

834 delims = f"{lineComment}" 

835 else: 

836 delims = None 

837 if delims: 

838 d = g.app.language_delims_dict 

839 if not d.get(self.language): 

840 d[self.language] = delims 

841 #@+node:ekr.20190324050727.1: *3* bjc.init_style_ivars 

842 def init_style_ivars(self): 

843 """Init Style data common to JEdit and Pygments colorizers.""" 

844 # init() properly sets these for each language. 

845 self.actualColorDict = {} # Used only by setTag. 

846 self.hyperCount = 0 

847 # Attributes dict ivars: defaults are as shown... 

848 self.default = 'null' 

849 self.digit_re = '' 

850 self.escape = '' 

851 self.highlight_digits = True 

852 self.ignore_case = True 

853 self.no_word_sep = '' 

854 # Debugging... 

855 self.allow_mark_prev = True 

856 self.n_setTag = 0 

857 self.tagCount = 0 

858 self.trace_leo_matches = False 

859 self.trace_match_flag = False 

860 # Profiling... 

861 self.recolorCount = 0 # Total calls to recolor 

862 self.stateCount = 0 # Total calls to setCurrentState 

863 self.totalStates = 0 

864 self.maxStateNumber = 0 

865 self.totalKeywordsCalls = 0 

866 self.totalLeoKeywordsCalls = 0 

867 # Mode data... 

868 self.defaultRulesList = [] 

869 self.importedRulesets = {} 

870 self.initLanguage = None 

871 self.prev = None # The previous token. 

872 self.fonts = {} # Keys are config names. Values are actual fonts. 

873 self.keywords = {} # Keys are keywords, values are 0..5. 

874 # Keys are state ints, values are language names. 

875 self.modes = {} # Keys are languages, values are modes. 

876 self.mode = None # The mode object for the present language. 

877 self.modeBunch = None # A bunch fully describing a mode. 

878 self.modeStack = [] 

879 self.rulesDict = {} 

880 # self.defineAndExtendForthWords() 

881 self.word_chars = {} # Inited by init_keywords(). 

882 self.tags = [ 

883 # 8 Leo-specific tags. 

884 "blank", # show_invisibles_space_color 

885 "docpart", 

886 "leokeyword", 

887 "link", 

888 "name", 

889 "namebrackets", 

890 "tab", # show_invisibles_space_color 

891 "url", 

892 # jEdit tags. 

893 'comment1', 'comment2', 'comment3', 'comment4', 

894 # default, # exists, but never generated. 

895 'function', 

896 'keyword1', 'keyword2', 'keyword3', 'keyword4', 

897 'label', 'literal1', 'literal2', 'literal3', 'literal4', 

898 'markup', 'operator', 

899 'trailing_whitespace', 

900 ] 

901 #@+node:ekr.20110605121601.18587: *3* bjc.munge 

902 def munge(self, s): 

903 """Munge a mode name so that it is a valid python id.""" 

904 valid = string.ascii_letters + string.digits + '_' 

905 return ''.join([ch.lower() if ch in valid else '_' for ch in s]) 

906 #@+node:ekr.20171114041307.1: *3* bjc.reloadSettings & helper 

907 #@@nobeautify 

908 def reloadSettings(self): 

909 c, getBool = self.c, self.c.config.getBool 

910 # 

911 # Init all settings ivars. 

912 self.color_tags_list = [] 

913 self.showInvisibles = getBool("show-invisibles-by-default") 

914 self.underline_undefined = getBool("underline-undefined-section-names") 

915 self.use_hyperlinks = getBool("use-hyperlinks") 

916 self.use_pygments = None # Set in report_changes. 

917 self.use_pygments_styles = getBool('use-pygments-styles', default=True) 

918 # 

919 # Report changes to pygments settings. 

920 self.report_changes() 

921 # 

922 # Init the default fonts. 

923 self.bold_font = c.config.getFontFromParams( 

924 "body_text_font_family", "body_text_font_size", 

925 "body_text_font_slant", "body_text_font_weight", 

926 c.config.defaultBodyFontSize) 

927 self.italic_font = c.config.getFontFromParams( 

928 "body_text_font_family", "body_text_font_size", 

929 "body_text_font_slant", "body_text_font_weight", 

930 c.config.defaultBodyFontSize) 

931 self.bolditalic_font = c.config.getFontFromParams( 

932 "body_text_font_family", "body_text_font_size", 

933 "body_text_font_slant", "body_text_font_weight", 

934 c.config.defaultBodyFontSize) 

935 #@+node:ekr.20190327053604.1: *4* bjc.report_changes 

936 prev_use_pygments = None 

937 prev_use_styles = None 

938 prev_style = None 

939 

940 def report_changes(self): 

941 """Report changes to pygments settings""" 

942 c = self.c 

943 use_pygments = c.config.getBool('use-pygments', default=False) 

944 if not use_pygments: # 1696. 

945 return 

946 trace = 'coloring' in g.app.debug and not g.unitTesting 

947 if trace: 

948 g.es_print('\nreport changes...') 

949 

950 def show(setting, val): 

951 if trace: 

952 g.es_print(f"{setting:35}: {val}") 

953 

954 # 

955 # Set self.use_pygments only once: it can't be changed later. 

956 # There is no easy way to re-instantiate classes created by make_colorizer. 

957 if self.prev_use_pygments is None: 

958 self.use_pygments = self.prev_use_pygments = use_pygments 

959 show('@bool use-pygments', use_pygments) 

960 elif use_pygments == self.prev_use_pygments: 

961 show('@bool use-pygments', use_pygments) 

962 else: 

963 g.es_print( 

964 f"{'Can not change @bool use-pygments':35}: " 

965 f"{self.prev_use_pygments}", 

966 color='red') 

967 # 

968 # Report everything if we are tracing. 

969 style_name = c.config.getString('pygments-style-name') or 'default' 

970 # Don't set an ivar. It's not used in this class. 

971 # This setting is used only in the LeoHighlighter class 

972 show('@bool use-pytments-styles', self.use_pygments_styles) 

973 show('@string pygments-style-name', style_name) 

974 # 

975 # Report changes to @bool use-pygments-style 

976 if self.prev_use_styles is None: 

977 self.prev_use_styles = self.use_pygments_styles 

978 elif self.use_pygments_styles != self.prev_use_styles: 

979 g.es_print(f"using pygments styles: {self.use_pygments_styles}") 

980 # 

981 # Report @string pygments-style-name only if we are using styles. 

982 if not self.use_pygments_styles: 

983 return 

984 # 

985 # Report changes to @string pygments-style-name 

986 if self.prev_style is None: 

987 self.prev_style = style_name 

988 elif style_name != self.prev_style: 

989 g.es_print(f"New pygments style: {style_name}") 

990 self.prev_style = style_name 

991 #@+node:ekr.20110605121601.18641: *3* bjc.setTag 

992 last_v = None 

993 

994 def setTag(self, tag, s, i, j): 

995 """Set the tag in the highlighter.""" 

996 trace = 'coloring' in g.app.debug and not g.unitTesting 

997 self.n_setTag += 1 

998 if i == j: 

999 return 

1000 wrapper = self.wrapper # A QTextEditWrapper 

1001 if not tag.strip(): 

1002 return 

1003 tag = tag.lower().strip() 

1004 # A hack to allow continuation dots on any tag. 

1005 dots = tag.startswith('dots') 

1006 if dots: 

1007 tag = tag[len('dots') :] 

1008 colorName = wrapper.configDict.get(tag) 

1009 # This color name should already be valid. 

1010 if not colorName: 

1011 return 

1012 # 

1013 # New in Leo 5.8.1: allow symbolic color names here. 

1014 # This now works because all keys in leo_color_database are normalized. 

1015 colorName = colorName.replace( 

1016 ' ', '').replace('-', '').replace('_', '').lower().strip() 

1017 colorName = leo_color_database.get(colorName, colorName) 

1018 # Get the actual color. 

1019 color = self.actualColorDict.get(colorName) 

1020 if not color: 

1021 color = QtGui.QColor(colorName) 

1022 if color.isValid(): 

1023 self.actualColorDict[colorName] = color 

1024 else: 

1025 g.trace('unknown color name', colorName, g.callers()) 

1026 return 

1027 underline = wrapper.configUnderlineDict.get(tag) 

1028 format = QtGui.QTextCharFormat() 

1029 font = self.fonts.get(tag) 

1030 if font: 

1031 format.setFont(font) 

1032 self.configure_hard_tab_width(font) # #1919. 

1033 if tag in ('blank', 'tab'): 

1034 if tag == 'tab' or colorName == 'black': 

1035 format.setFontUnderline(True) 

1036 if colorName != 'black': 

1037 format.setBackground(color) 

1038 elif underline: 

1039 format.setForeground(color) 

1040 format.setUnderlineStyle(UnderlineStyle.SingleUnderline) 

1041 format.setFontUnderline(True) 

1042 elif dots or tag == 'trailing_whitespace': 

1043 format.setForeground(color) 

1044 format.setUnderlineStyle(UnderlineStyle.DotLine) 

1045 else: 

1046 format.setForeground(color) 

1047 format.setUnderlineStyle(UnderlineStyle.NoUnderline) 

1048 self.tagCount += 1 

1049 if trace: 

1050 # A superb trace. 

1051 if len(repr(s[i:j])) <= 20: 

1052 s2 = repr(s[i:j]) 

1053 else: 

1054 s2 = repr(s[i : i + 17 - 2] + '...') 

1055 kind_s = f"{self.language}.{tag}" 

1056 kind_s2 = f"{self.delegate_name}:" if self.delegate_name else '' 

1057 print( 

1058 f"setTag: {kind_s:25} {i:3} {j:3} {s2:>20} " 

1059 f"{self.rulesetName}:{kind_s2}{self.matcher_name}" 

1060 ) 

1061 self.highlighter.setFormat(i, j - i, format) 

1062 #@-others 

1063#@+node:ekr.20110605121601.18569: ** class JEditColorizer(BaseJEditColorizer) 

1064# This is c.frame.body.colorizer 

1065 

1066 

1067class JEditColorizer(BaseJEditColorizer): 

1068 """ 

1069 The JEditColorizer class adapts jEdit pattern matchers for QSyntaxHighlighter. 

1070 For full documentation, see: 

1071 https://github.com/leo-editor/leo-editor/blob/master/leo/doc/colorizer.md 

1072 """ 

1073 #@+others 

1074 #@+node:ekr.20110605121601.18572: *3* jedit.__init__ & helpers 

1075 def __init__(self, c, widget, wrapper): 

1076 """Ctor for JEditColorizer class.""" 

1077 super().__init__(c, widget, wrapper) 

1078 # 

1079 # Create the highlighter. The default is NullObject. 

1080 if isinstance(widget, QtWidgets.QTextEdit): 

1081 self.highlighter = LeoHighlighter(c, 

1082 colorizer=self, 

1083 document=widget.document(), 

1084 ) 

1085 # 

1086 # State data used only by this class... 

1087 self.after_doc_language = None 

1088 self.initialStateNumber = -1 

1089 self.old_v = None 

1090 self.nextState = 1 # Dont use 0. 

1091 self.n2languageDict = {-1: c.target_language} 

1092 self.restartDict = {} # Keys are state numbers, values are restart functions. 

1093 self.stateDict = {} # Keys are state numbers, values state names. 

1094 self.stateNameDict = {} # Keys are state names, values are state numbers. 

1095 # #2276: Set by init_section_delims. 

1096 self.section_delim1 = '<<' 

1097 self.section_delim2 = '>>' 

1098 # 

1099 # Init common data... 

1100 self.reloadSettings() 

1101 #@+node:ekr.20110605121601.18580: *4* jedit.init 

1102 def init(self, p=None): 

1103 """Init the colorizer, but *not* state.""" 

1104 # 

1105 # These *must* be recomputed. 

1106 self.initialStateNumber = self.setInitialStateNumber() 

1107 # 

1108 # Fix #389. Do *not* change these. 

1109 # self.nextState = 1 # Dont use 0. 

1110 # self.stateDict = {} 

1111 # self.stateNameDict = {} 

1112 # self.restartDict = {} 

1113 self.init_mode(self.language) 

1114 self.clearState() 

1115 # Used by matchers. 

1116 self.prev = None 

1117 # Must be done to support per-language @font/@color settings. 

1118 self.configure_tags() 

1119 self.init_section_delims() # #2276 

1120 #@+node:ekr.20170201082248.1: *4* jedit.init_all_state 

1121 def init_all_state(self, v): 

1122 """Completely init all state data.""" 

1123 assert self.language, g.callers(8) 

1124 self.old_v = v 

1125 self.n2languageDict = {-1: self.language} 

1126 self.nextState = 1 # Dont use 0. 

1127 self.restartDict = {} 

1128 self.stateDict = {} 

1129 self.stateNameDict = {} 

1130 #@+node:ekr.20211029073553.1: *4* jedit.init_section_delims 

1131 def init_section_delims(self): 

1132 

1133 p = self.c.p 

1134 

1135 def find_delims(v): 

1136 for s in g.splitLines(v.b): 

1137 m = g.g_section_delims_pat.match(s) 

1138 if m: 

1139 return m 

1140 return None 

1141 

1142 v = g.findAncestorVnodeByPredicate(p, v_predicate=find_delims) 

1143 if v: 

1144 m = find_delims(v) 

1145 self.section_delim1 = m.group(1) 

1146 self.section_delim2 = m.group(2) 

1147 else: 

1148 self.section_delim1 = '<<' 

1149 self.section_delim2 = '>>' 

1150 #@+node:ekr.20190326183005.1: *4* jedit.reloadSettings 

1151 def reloadSettings(self): 

1152 """Complete the initialization of all settings.""" 

1153 if 'coloring' in g.app.debug and not g.unitTesting: 

1154 print('jedit.reloadSettings.') 

1155 # Do the basic inits. 

1156 BaseJEditColorizer.reloadSettings(self) 

1157 # Init everything else. 

1158 self.init_style_ivars() 

1159 self.defineLeoKeywordsDict() 

1160 self.defineDefaultColorsDict() 

1161 self.defineDefaultFontDict() 

1162 self.init() 

1163 #@+node:ekr.20110605121601.18589: *3* jedit.Pattern matchers 

1164 #@+node:ekr.20110605121601.18590: *4* About the pattern matchers 

1165 #@@language rest 

1166 #@+at 

1167 # The following jEdit matcher methods return the length of the matched text if the 

1168 # match succeeds, and zero otherwise. In most cases, these methods colorize all 

1169 # the matched text. 

1170 # 

1171 # The following arguments affect matching: 

1172 # 

1173 # - at_line_start True: sequence must start the line. 

1174 # - at_whitespace_end True: sequence must be first non-whitespace text of the line. 

1175 # - at_word_start True: sequence must start a word. 

1176 # - hash_char The first character that must match in a regular expression. 

1177 # - no_escape: True: ignore an 'end' string if it is preceded by 

1178 # the ruleset's escape character. 

1179 # - no_line_break True: the match will not succeed across line breaks. 

1180 # - no_word_break: True: the match will not cross word breaks. 

1181 # 

1182 # The following arguments affect coloring when a match succeeds: 

1183 # 

1184 # - delegate A ruleset name. The matched text will be colored recursively 

1185 # by the indicated ruleset. 

1186 # - exclude_match If True, the actual text that matched will not be colored. 

1187 # - kind The color tag to be applied to colored text. 

1188 #@+node:ekr.20110605121601.18591: *4* jedit.dump 

1189 def dump(self, s): 

1190 if s.find('\n') == -1: 

1191 return s 

1192 return '\n' + s + '\n' 

1193 #@+node:ekr.20110605121601.18592: *4* jedit.Leo rule functions 

1194 #@+node:ekr.20110605121601.18593: *5* jedit.match_at_color 

1195 def match_at_color(self, s, i): 

1196 if self.trace_leo_matches: 

1197 g.trace() 

1198 # Only matches at start of line. 

1199 if i == 0 and g.match_word(s, 0, '@color'): 

1200 n = self.setRestart(self.restartColor) 

1201 self.setState(n) # Enable coloring of *this* line. 

1202 self.colorRangeWithTag(s, 0, len('@color'), 'leokeyword') 

1203 # Now required. Sets state. 

1204 return len('@color') 

1205 return 0 

1206 #@+node:ekr.20170125140113.1: *6* restartColor 

1207 def restartColor(self, s): 

1208 """Change all lines up to the next color directive.""" 

1209 if g.match_word(s, 0, '@killcolor'): 

1210 self.colorRangeWithTag(s, 0, len('@color'), 'leokeyword') 

1211 self.setRestart(self.restartKillColor) 

1212 return -len(s) # Continue to suppress coloring. 

1213 if g.match_word(s, 0, '@nocolor-node'): 

1214 self.setRestart(self.restartNoColorNode) 

1215 return -len(s) # Continue to suppress coloring. 

1216 if g.match_word(s, 0, '@nocolor'): 

1217 self.setRestart(self.restartNoColor) 

1218 return -len(s) # Continue to suppress coloring. 

1219 n = self.setRestart(self.restartColor) 

1220 self.setState(n) # Enables coloring of *this* line. 

1221 return 0 # Allow colorizing! 

1222 #@+node:ekr.20110605121601.18597: *5* jedit.match_at_killcolor & restarter 

1223 def match_at_killcolor(self, s, i): 

1224 

1225 # Only matches at start of line. 

1226 if i == 0 and g.match_word(s, i, '@killcolor'): 

1227 self.setRestart(self.restartKillColor) 

1228 return len(s) # Match everything. 

1229 return 0 

1230 #@+node:ekr.20110605121601.18598: *6* jedit.restartKillColor 

1231 def restartKillColor(self, s): 

1232 self.setRestart(self.restartKillColor) 

1233 return len(s) + 1 

1234 #@+node:ekr.20110605121601.18594: *5* jedit.match_at_language 

1235 def match_at_language(self, s, i): 

1236 """Match Leo's @language directive.""" 

1237 # Only matches at start of line. 

1238 if i != 0: 

1239 return 0 

1240 if g.match_word(s, i, '@language'): 

1241 old_name = self.language 

1242 j = g.skip_ws(s, i + len('@language')) 

1243 k = g.skip_c_id(s, j) 

1244 name = s[j:k] 

1245 ok = self.init_mode(name) 

1246 if ok: 

1247 self.colorRangeWithTag(s, i, k, 'leokeyword') 

1248 if name != old_name: 

1249 # Solves the recoloring problem! 

1250 n = self.setInitialStateNumber() 

1251 self.setState(n) 

1252 return k - i 

1253 return 0 

1254 #@+node:ekr.20110605121601.18595: *5* jedit.match_at_nocolor & restarter 

1255 def match_at_nocolor(self, s, i): 

1256 

1257 if self.trace_leo_matches: 

1258 g.trace(i, repr(s)) 

1259 # Only matches at start of line. 

1260 if i == 0 and not g.match(s, i, '@nocolor-') and g.match_word(s, i, '@nocolor'): 

1261 self.setRestart(self.restartNoColor) 

1262 return len(s) # Match everything. 

1263 return 0 

1264 #@+node:ekr.20110605121601.18596: *6* jedit.restartNoColor 

1265 def restartNoColor(self, s): 

1266 if self.trace_leo_matches: 

1267 g.trace(repr(s)) 

1268 if g.match_word(s, 0, '@color'): 

1269 n = self.setRestart(self.restartColor) 

1270 self.setState(n) # Enables coloring of *this* line. 

1271 self.colorRangeWithTag(s, 0, len('@color'), 'leokeyword') 

1272 return len('@color') 

1273 self.setRestart(self.restartNoColor) 

1274 return len(s) # Match everything. 

1275 #@+node:ekr.20110605121601.18599: *5* jedit.match_at_nocolor_node & restarter 

1276 def match_at_nocolor_node(self, s, i): 

1277 

1278 # Only matches at start of line. 

1279 if i == 0 and g.match_word(s, i, '@nocolor-node'): 

1280 self.setRestart(self.restartNoColorNode) 

1281 return len(s) # Match everything. 

1282 return 0 

1283 #@+node:ekr.20110605121601.18600: *6* jedit.restartNoColorNode 

1284 def restartNoColorNode(self, s): 

1285 self.setRestart(self.restartNoColorNode) 

1286 return len(s) + 1 

1287 #@+node:ekr.20150622072456.1: *5* jedit.match_at_wrap 

1288 def match_at_wrap(self, s, i): 

1289 """Match Leo's @wrap directive.""" 

1290 c = self.c 

1291 # Only matches at start of line. 

1292 seq = '@wrap' 

1293 if i == 0 and g.match_word(s, i, seq): 

1294 j = i + len(seq) 

1295 k = g.skip_ws(s, j) 

1296 self.colorRangeWithTag(s, i, k, 'leokeyword') 

1297 c.frame.forceWrap(c.p) 

1298 return k - i 

1299 return 0 

1300 #@+node:ekr.20110605121601.18601: *5* jedit.match_blanks 

1301 def match_blanks(self, s, i): 

1302 # Use Qt code to show invisibles. 

1303 return 0 

1304 #@+node:ekr.20110605121601.18602: *5* jedit.match_doc_part & restarter 

1305 def match_doc_part(self, s, i): 

1306 """ 

1307 Colorize Leo's @ and @ doc constructs. 

1308 Matches only at the start of the line. 

1309 """ 

1310 if i != 0: 

1311 return 0 

1312 if g.match_word(s, i, '@doc'): 

1313 j = i + 4 

1314 elif g.match(s, i, '@') and (i + 1 >= len(s) or s[i + 1] in (' ', '\t', '\n')): 

1315 j = i + 1 

1316 else: 

1317 return 0 

1318 c = self.c 

1319 self.colorRangeWithTag(s, 0, j, 'leokeyword') 

1320 # New in Leo 5.5: optionally colorize doc parts using reStructuredText 

1321 if c.config.getBool('color-doc-parts-as-rest'): 

1322 # Switch langauges. 

1323 self.after_doc_language = self.language 

1324 self.language = 'rest' 

1325 self.clearState() 

1326 self.init(c.p) 

1327 # Restart. 

1328 self.setRestart(self.restartDocPart) 

1329 # Do *not* color the text here! 

1330 return j 

1331 self.clearState() 

1332 self.setRestart(self.restartDocPart) 

1333 self.colorRangeWithTag(s, j, len(s), 'docpart') 

1334 return len(s) 

1335 #@+node:ekr.20110605121601.18603: *6* jedit.restartDocPart 

1336 def restartDocPart(self, s): 

1337 """ 

1338 Restarter for @ and @ contructs. 

1339 Continue until an @c, @code or @language at the start of the line. 

1340 """ 

1341 for tag in ('@c', '@code', '@language'): 

1342 if g.match_word(s, 0, tag): 

1343 if tag == '@language': 

1344 return self.match_at_language(s, 0) 

1345 j = len(tag) 

1346 self.colorRangeWithTag(s, 0, j, 'leokeyword') # 'docpart') 

1347 # Switch languages. 

1348 self.language = self.after_doc_language 

1349 self.clearState() 

1350 self.init(self.c.p) 

1351 self.after_doc_language = None 

1352 return j 

1353 # Color the next line. 

1354 self.setRestart(self.restartDocPart) 

1355 if self.c.config.getBool('color-doc-parts-as-rest'): 

1356 # Do *not* colorize the text here. 

1357 return 0 

1358 self.colorRangeWithTag(s, 0, len(s), 'docpart') 

1359 return len(s) 

1360 #@+node:ekr.20170204072452.1: *5* jedit.match_image 

1361 image_url = re.compile(r'^\s*<\s*img\s+.*src=\"(.*)\".*>\s*$') 

1362 

1363 def match_image(self, s, i): 

1364 """Matcher for <img...>""" 

1365 m = self.image_url.match(s, i) 

1366 if m: 

1367 self.image_src = src = m.group(1) 

1368 j = len(src) 

1369 doc = self.highlighter.document() 

1370 block_n = self.currentBlockNumber() 

1371 text_block = doc.findBlockByNumber(block_n) 

1372 g.trace(f"block_n: {block_n:2} {s!r}") 

1373 g.trace(f"block text: {repr(text_block.text())}") 

1374 # How to get the cursor of the colorized line. 

1375 # body = self.c.frame.body 

1376 # s = body.wrapper.getAllText() 

1377 # wrapper.delete(0, j) 

1378 # cursor.insertHtml(src) 

1379 return j 

1380 return 0 

1381 #@+node:ekr.20110605121601.18604: *5* jedit.match_leo_keywords 

1382 def match_leo_keywords(self, s, i): 

1383 """Succeed if s[i:] is a Leo keyword.""" 

1384 self.totalLeoKeywordsCalls += 1 

1385 if s[i] != '@': 

1386 return 0 

1387 # fail if something besides whitespace precedes the word on the line. 

1388 i2 = i - 1 

1389 while i2 >= 0: 

1390 ch = s[i2] 

1391 if ch == '\n': 

1392 break 

1393 elif ch in (' ', '\t'): 

1394 i2 -= 1 

1395 else: 

1396 return 0 

1397 # Get the word as quickly as possible. 

1398 j = i + 1 

1399 while j < len(s) and s[j] in self.word_chars: 

1400 j += 1 

1401 word = s[i + 1 : j] # entries in leoKeywordsDict do not start with '@'. 

1402 if j < len(s) and s[j] not in (' ', '\t', '\n'): 

1403 return 0 # Fail, but allow a rescan, as in objective_c. 

1404 if self.leoKeywordsDict.get(word): 

1405 kind = 'leokeyword' 

1406 self.colorRangeWithTag(s, i, j, kind) 

1407 self.prev = (i, j, kind) 

1408 result = j - i + 1 # Bug fix: skip the last character. 

1409 self.trace_match(kind, s, i, j) 

1410 return result 

1411 # 2010/10/20: also check the keywords dict here. 

1412 # This allows for objective_c keywords starting with '@' 

1413 # This will not slow down Leo, because it is called 

1414 # for things that look like Leo directives. 

1415 word = '@' + word 

1416 kind = self.keywordsDict.get(word) 

1417 if kind: 

1418 self.colorRangeWithTag(s, i, j, kind) 

1419 self.prev = (i, j, kind) 

1420 self.trace_match(kind, s, i, j) 

1421 return j - i 

1422 # Bug fix: allow rescan. Affects @language patch. 

1423 return 0 

1424 #@+node:ekr.20110605121601.18605: *5* jedit.match_section_ref 

1425 def match_section_ref(self, s, i): 

1426 p = self.c.p 

1427 if self.trace_leo_matches: 

1428 g.trace(self.section_delim1, self.section_delim2, s) 

1429 # 

1430 # Special case for @language patch: section references are not honored. 

1431 if self.language == 'patch': 

1432 return 0 

1433 n1, n2 = len(self.section_delim1), len(self.section_delim2) 

1434 if not g.match(s, i, self.section_delim1): 

1435 return 0 

1436 k = g.find_on_line(s, i + n1, self.section_delim2) 

1437 if k == -1: 

1438 return 0 

1439 j = k + n2 

1440 # Special case for @section-delims. 

1441 if s.startswith('@section-delims'): 

1442 self.colorRangeWithTag(s, i, i + n1, 'namebrackets') 

1443 self.colorRangeWithTag(s, k, j, 'namebrackets') 

1444 return j - i 

1445 # An actual section reference. 

1446 self.colorRangeWithTag(s, i, i + n1, 'namebrackets') 

1447 ref = g.findReference(s[i:j], p) 

1448 if ref: 

1449 if self.use_hyperlinks: 

1450 #@+<< set the hyperlink >> 

1451 #@+node:ekr.20110605121601.18606: *6* << set the hyperlink >> (jedit) 

1452 # Set the bindings to VNode callbacks. 

1453 tagName = "hyper" + str(self.hyperCount) 

1454 self.hyperCount += 1 

1455 ref.tagName = tagName 

1456 #@-<< set the hyperlink >> 

1457 else: 

1458 self.colorRangeWithTag(s, i + n1, k, 'link') 

1459 else: 

1460 self.colorRangeWithTag(s, i + n1, k, 'name') 

1461 self.colorRangeWithTag(s, k, j, 'namebrackets') 

1462 return j - i 

1463 #@+node:ekr.20110605121601.18607: *5* jedit.match_tabs 

1464 def match_tabs(self, s, i): 

1465 # Use Qt code to show invisibles. 

1466 return 0 

1467 # Old code... 

1468 # if not self.showInvisibles: 

1469 # return 0 

1470 # if self.trace_leo_matches: g.trace() 

1471 # j = i; n = len(s) 

1472 # while j < n and s[j] == '\t': 

1473 # j += 1 

1474 # if j > i: 

1475 # self.colorRangeWithTag(s, i, j, 'tab') 

1476 # return j - i 

1477 # return 0 

1478 #@+node:tbrown.20170707150713.1: *5* jedit.match_tabs 

1479 def match_trailing_ws(self, s, i): 

1480 """match trailing whitespace""" 

1481 j = i 

1482 n = len(s) 

1483 while j < n and s[j] in ' \t': 

1484 j += 1 

1485 if j > i and j == n: 

1486 self.colorRangeWithTag(s, i, j, 'trailing_whitespace') 

1487 return j - i 

1488 return 0 

1489 #@+node:ekr.20170225103140.1: *5* jedit.match_unl 

1490 def match_unl(self, s, i): 

1491 if g.match(s.lower(), i, 'unl://'): 

1492 j = len(s) # By default, color the whole line. 

1493 # #2410: Limit the coloring if possible. 

1494 if i > 0: 

1495 ch = s[i-1] 

1496 if ch in ('"', "'", '`'): 

1497 k = s.find(ch, i) 

1498 if k > -1: 

1499 j = k 

1500 self.colorRangeWithTag(s, i, j, 'url') 

1501 return j 

1502 return 0 

1503 #@+node:ekr.20110605121601.18608: *5* jedit.match_url_any/f/h 

1504 # Fix bug 893230: URL coloring does not work for many Internet protocols. 

1505 # Added support for: gopher, mailto, news, nntp, prospero, telnet, wais 

1506 

1507 url_regex_f = re.compile(r"""(file|ftp)://[^\s'"]+[\w=/]""") 

1508 url_regex_g = re.compile(r"""gopher://[^\s'"]+[\w=/]""") 

1509 url_regex_h = re.compile(r"""(http|https)://[^\s'"]+[\w=/]""") 

1510 url_regex_m = re.compile(r"""mailto://[^\s'"]+[\w=/]""") 

1511 url_regex_n = re.compile(r"""(news|nntp)://[^\s'"]+[\w=/]""") 

1512 url_regex_p = re.compile(r"""prospero://[^\s'"]+[\w=/]""") 

1513 url_regex_t = re.compile(r"""telnet://[^\s'"]+[\w=/]""") 

1514 url_regex_w = re.compile(r"""wais://[^\s'"]+[\w=/]""") 

1515 kinds = '(file|ftp|gopher|http|https|mailto|news|nntp|prospero|telnet|wais)' 

1516 url_regex = re.compile(fr"""{kinds}://[^\s'"]+[\w=/]""") 

1517 

1518 def match_any_url(self, s, i): 

1519 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex) 

1520 

1521 def match_url_f(self, s, i): 

1522 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_f) 

1523 

1524 def match_url_g(self, s, i): 

1525 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_g) 

1526 

1527 def match_url_h(self, s, i): 

1528 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_h) 

1529 

1530 def match_url_m(self, s, i): 

1531 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_m) 

1532 

1533 def match_url_n(self, s, i): 

1534 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_n) 

1535 

1536 def match_url_p(self, s, i): 

1537 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_p) 

1538 

1539 def match_url_t(self, s, i): 

1540 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_t) 

1541 

1542 def match_url_w(self, s, i): 

1543 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_w) 

1544 #@+node:ekr.20110605121601.18609: *4* jedit.match_compiled_regexp 

1545 def match_compiled_regexp(self, s, i, kind, regexp, delegate=''): 

1546 """Succeed if the compiled regular expression regexp matches at s[i:].""" 

1547 n = self.match_compiled_regexp_helper(s, i, regexp) 

1548 if n > 0: 

1549 j = i + n 

1550 self.colorRangeWithTag(s, i, j, kind, delegate=delegate) 

1551 self.prev = (i, j, kind) 

1552 self.trace_match(kind, s, i, j) 

1553 return n 

1554 return 0 

1555 #@+node:ekr.20110605121601.18610: *5* jedit.match_compiled_regexp_helper 

1556 def match_compiled_regexp_helper(self, s, i, regex): 

1557 """ 

1558 Return the length of the matching text if 

1559 seq (a regular expression) matches the present position. 

1560 """ 

1561 # Match succeeds or fails more quickly than search. 

1562 self.match_obj = mo = regex.match(s, i) # re_obj.search(s,i) 

1563 if mo is None: 

1564 return 0 

1565 start, end = mo.start(), mo.end() 

1566 if start != i: 

1567 return 0 

1568 return end - start 

1569 #@+node:ekr.20110605121601.18611: *4* jedit.match_eol_span 

1570 def match_eol_span(self, s, i, 

1571 kind=None, seq='', 

1572 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1573 delegate='', exclude_match=False 

1574 ): 

1575 """Succeed if seq matches s[i:]""" 

1576 if at_line_start and i != 0 and s[i - 1] != '\n': 

1577 return 0 

1578 if at_whitespace_end and i != g.skip_ws(s, 0): 

1579 return 0 

1580 if at_word_start and i > 0 and s[i - 1] in self.word_chars: 

1581 return 0 

1582 if at_word_start and i + len( 

1583 seq) + 1 < len(s) and s[i + len(seq)] in self.word_chars: 

1584 return 0 

1585 if g.match(s, i, seq): 

1586 j = len(s) 

1587 self.colorRangeWithTag( 

1588 s, i, j, kind, delegate=delegate, exclude_match=exclude_match) 

1589 self.prev = (i, j, kind) 

1590 self.trace_match(kind, s, i, j) 

1591 return j # (was j-1) With a delegate, this could clear state. 

1592 return 0 

1593 #@+node:ekr.20110605121601.18612: *4* jedit.match_eol_span_regexp 

1594 def match_eol_span_regexp(self, s, i, 

1595 kind='', regexp='', 

1596 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1597 delegate='', exclude_match=False 

1598 ): 

1599 """Succeed if the regular expression regex matches s[i:].""" 

1600 if at_line_start and i != 0 and s[i - 1] != '\n': 

1601 return 0 

1602 if at_whitespace_end and i != g.skip_ws(s, 0): 

1603 return 0 

1604 if at_word_start and i > 0 and s[i - 1] in self.word_chars: 

1605 return 0 # 7/5/2008 

1606 n = self.match_regexp_helper(s, i, regexp) 

1607 if n > 0: 

1608 j = len(s) 

1609 self.colorRangeWithTag( 

1610 s, i, j, kind, delegate=delegate, exclude_match=exclude_match) 

1611 self.prev = (i, j, kind) 

1612 self.trace_match(kind, s, i, j) 

1613 return j - i 

1614 return 0 

1615 #@+node:ekr.20110605121601.18613: *4* jedit.match_everything 

1616 # def match_everything (self,s,i,kind=None,delegate='',exclude_match=False): 

1617 # """Match the entire rest of the string.""" 

1618 # j = len(s) 

1619 # self.colorRangeWithTag(s,i,j,kind,delegate=delegate) 

1620 # return j 

1621 #@+node:ekr.20110605121601.18614: *4* jedit.match_keywords 

1622 # This is a time-critical method. 

1623 

1624 def match_keywords(self, s, i): 

1625 """ 

1626 Succeed if s[i:] is a keyword. 

1627 Returning -len(word) for failure greatly reduces the number of times this 

1628 method is called. 

1629 """ 

1630 self.totalKeywordsCalls += 1 

1631 # We must be at the start of a word. 

1632 if i > 0 and s[i - 1] in self.word_chars: 

1633 return 0 

1634 # Get the word as quickly as possible. 

1635 j = i 

1636 n = len(s) 

1637 chars = self.word_chars 

1638 # Special cases... 

1639 if self.language in ('haskell', 'clojure'): 

1640 chars["'"] = "'" 

1641 if self.language == 'c': 

1642 chars['_'] = '_' 

1643 while j < n and s[j] in chars: 

1644 j += 1 

1645 word = s[i:j] 

1646 # Fix part of #585: A kludge for css. 

1647 if self.language == 'css' and word.endswith(':'): 

1648 j -= 1 

1649 word = word[:-1] 

1650 if not word: 

1651 g.trace( 

1652 'can not happen', 

1653 repr(s[i : max(j, i + 1)]), 

1654 repr(s[i : i + 10]), 

1655 g.callers(), 

1656 ) 

1657 return 0 

1658 if self.ignore_case: 

1659 word = word.lower() 

1660 kind = self.keywordsDict.get(word) 

1661 if kind: 

1662 self.colorRangeWithTag(s, i, j, kind) 

1663 self.prev = (i, j, kind) 

1664 result = j - i 

1665 self.trace_match(kind, s, i, j) 

1666 return result 

1667 return -len(word) # An important new optimization. 

1668 #@+node:ekr.20110605121601.18615: *4* jedit.match_line 

1669 def match_line(self, s, i, kind=None, delegate='', exclude_match=False): 

1670 """Match the rest of the line.""" 

1671 j = g.skip_to_end_of_line(s, i) 

1672 self.colorRangeWithTag(s, i, j, kind, delegate=delegate) 

1673 return j - i 

1674 #@+node:ekr.20190606201152.1: *4* jedit.match_lua_literal 

1675 def match_lua_literal(self, s, i, kind): 

1676 """Succeed if s[i:] is a lua literal. See #1175""" 

1677 k = self.match_span(s, i, kind=kind, begin="[[", end="]]") 

1678 if k not in (None, 0): 

1679 return k 

1680 if not g.match(s, i, '[='): 

1681 return 0 

1682 # Calculate begin and end, then just call match_span 

1683 j = i + 2 

1684 while g.match(s, j, '='): 

1685 j += 1 

1686 if not g.match(s, j, '['): 

1687 return 0 

1688 return self.match_span(s, i, kind=kind, begin=s[i:j], end=s[i + 1 : j] + ']') 

1689 #@+node:ekr.20110605121601.18616: *4* jedit.match_mark_following & getNextToken 

1690 def match_mark_following(self, s, i, 

1691 kind='', pattern='', 

1692 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1693 exclude_match=False 

1694 ): 

1695 """Succeed if s[i:] matches pattern.""" 

1696 if not self.allow_mark_prev: 

1697 return 0 

1698 if at_line_start and i != 0 and s[i - 1] != '\n': 

1699 return 0 

1700 if at_whitespace_end and i != g.skip_ws(s, 0): 

1701 return 0 

1702 if at_word_start and i > 0 and s[i - 1] in self.word_chars: 

1703 return 0 # 7/5/2008 

1704 if ( 

1705 at_word_start 

1706 and i + len(pattern) + 1 < len(s) 

1707 and s[i + len(pattern)] in self.word_chars 

1708 ): 

1709 return 0 

1710 if g.match(s, i, pattern): 

1711 j = i + len(pattern) 

1712 # self.colorRangeWithTag(s,i,j,kind,exclude_match=exclude_match) 

1713 k = self.getNextToken(s, j) 

1714 # 2011/05/31: Do not match *anything* unless there is a token following. 

1715 if k > j: 

1716 self.colorRangeWithTag(s, i, j, kind, exclude_match=exclude_match) 

1717 self.colorRangeWithTag(s, j, k, kind, exclude_match=False) 

1718 j = k 

1719 self.prev = (i, j, kind) 

1720 self.trace_match(kind, s, i, j) 

1721 return j - i 

1722 return 0 

1723 #@+node:ekr.20110605121601.18617: *5* jedit.getNextToken 

1724 def getNextToken(self, s, i): 

1725 """ 

1726 Return the index of the end of the next token for match_mark_following. 

1727 

1728 The jEdit docs are not clear about what a 'token' is, but experiments with jEdit 

1729 show that token means a word, as defined by word_chars. 

1730 """ 

1731 # 2011/05/31: Might we extend the concept of token? 

1732 # If s[i] is not a word char, should we return just it? 

1733 i0 = i 

1734 while i < len(s) and s[i].isspace(): 

1735 i += 1 

1736 i1 = i 

1737 while i < len(s) and s[i] in self.word_chars: 

1738 i += 1 

1739 if i == i1: 

1740 return i0 

1741 return min(len(s), i) 

1742 #@+node:ekr.20110605121601.18618: *4* jedit.match_mark_previous 

1743 def match_mark_previous(self, s, i, 

1744 kind='', pattern='', 

1745 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1746 exclude_match=False 

1747 ): 

1748 """ 

1749 Return the length of a matched SEQ or 0 if no match. 

1750 

1751 'at_line_start': True: sequence must start the line. 

1752 'at_whitespace_end':True: sequence must be first non-whitespace text of the line. 

1753 'at_word_start': True: sequence must start a word. 

1754 """ 

1755 # This match was causing most of the syntax-color problems. 

1756 return 0 # 2009/6/23 

1757 #@+node:ekr.20110605121601.18619: *4* jedit.match_regexp_helper 

1758 def match_regexp_helper(self, s, i, pattern): 

1759 """ 

1760 Return the length of the matching text if 

1761 seq (a regular expression) matches the present position. 

1762 """ 

1763 try: 

1764 flags = re.MULTILINE 

1765 if self.ignore_case: 

1766 flags |= re.IGNORECASE 

1767 re_obj = re.compile(pattern, flags) 

1768 except Exception: 

1769 # Do not call g.es here! 

1770 g.trace(f"Invalid regular expression: {pattern}") 

1771 return 0 

1772 # Match succeeds or fails more quickly than search. 

1773 self.match_obj = mo = re_obj.match(s, i) # re_obj.search(s,i) 

1774 if mo is None: 

1775 return 0 

1776 start, end = mo.start(), mo.end() 

1777 if start != i: # Bug fix 2007-12-18: no match at i 

1778 return 0 

1779 return end - start 

1780 #@+node:ekr.20110605121601.18620: *4* jedit.match_seq 

1781 def match_seq(self, s, i, 

1782 kind='', seq='', 

1783 at_line_start=False, 

1784 at_whitespace_end=False, 

1785 at_word_start=False, 

1786 delegate='' 

1787 ): 

1788 """Succeed if s[:] mathces seq.""" 

1789 if at_line_start and i != 0 and s[i - 1] != '\n': 

1790 j = i 

1791 elif at_whitespace_end and i != g.skip_ws(s, 0): 

1792 j = i 

1793 elif at_word_start and i > 0 and s[i - 1] in self.word_chars: # 7/5/2008 

1794 j = i 

1795 if at_word_start and i + len( 

1796 seq) + 1 < len(s) and s[i + len(seq)] in self.word_chars: 

1797 j = i # 7/5/2008 

1798 elif g.match(s, i, seq): 

1799 j = i + len(seq) 

1800 self.colorRangeWithTag(s, i, j, kind, delegate=delegate) 

1801 self.prev = (i, j, kind) 

1802 self.trace_match(kind, s, i, j) 

1803 else: 

1804 j = i 

1805 return j - i 

1806 #@+node:ekr.20110605121601.18621: *4* jedit.match_seq_regexp 

1807 def match_seq_regexp(self, s, i, 

1808 kind='', regexp='', 

1809 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1810 delegate='' 

1811 ): 

1812 """Succeed if the regular expression regexp matches at s[i:].""" 

1813 if at_line_start and i != 0 and s[i - 1] != '\n': 

1814 return 0 

1815 if at_whitespace_end and i != g.skip_ws(s, 0): 

1816 return 0 

1817 if at_word_start and i > 0 and s[i - 1] in self.word_chars: 

1818 return 0 

1819 n = self.match_regexp_helper(s, i, regexp) 

1820 j = i + n 

1821 assert j - i == n 

1822 self.colorRangeWithTag(s, i, j, kind, delegate=delegate) 

1823 self.prev = (i, j, kind) 

1824 self.trace_match(kind, s, i, j) 

1825 return j - i 

1826 #@+node:ekr.20110605121601.18622: *4* jedit.match_span & helper & restarter 

1827 def match_span(self, s, i, 

1828 kind='', begin='', end='', 

1829 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1830 delegate='', exclude_match=False, 

1831 no_escape=False, no_line_break=False, no_word_break=False 

1832 ): 

1833 """Succeed if s[i:] starts with 'begin' and contains a following 'end'.""" 

1834 dots = False # A flag that we are using dots as a continuation. 

1835 if i >= len(s): 

1836 return 0 

1837 if at_line_start and i != 0 and s[i - 1] != '\n': 

1838 j = i 

1839 elif at_whitespace_end and i != g.skip_ws(s, 0): 

1840 j = i 

1841 elif at_word_start and i > 0 and s[i - 1] in self.word_chars: 

1842 j = i 

1843 elif at_word_start and i + len( 

1844 begin) + 1 < len(s) and s[i + len(begin)] in self.word_chars: 

1845 j = i 

1846 elif not g.match(s, i, begin): 

1847 j = i 

1848 else: 

1849 # We have matched the start of the span. 

1850 j = self.match_span_helper(s, i + len(begin), end, 

1851 no_escape, no_line_break, no_word_break=no_word_break) 

1852 if j == -1: 

1853 j = i # A real failure. 

1854 else: 

1855 # A hack to handle continued strings. Should work for most languages. 

1856 # Prepend "dots" to the kind, as a flag to setTag. 

1857 dots = j > len( 

1858 s) and begin in "'\"" and end in "'\"" and kind.startswith('literal') 

1859 dots = dots and self.language not in ('lisp', 'elisp', 'rust') 

1860 if dots: 

1861 kind = 'dots' + kind 

1862 # A match 

1863 i2 = i + len(begin) 

1864 j2 = j + len(end) 

1865 if delegate: 

1866 self.colorRangeWithTag( 

1867 s, i, i2, kind, delegate=None, exclude_match=exclude_match) 

1868 self.colorRangeWithTag( 

1869 s, i2, j, kind, delegate=delegate, exclude_match=exclude_match) 

1870 self.colorRangeWithTag( 

1871 s, j, j2, kind, delegate=None, exclude_match=exclude_match) 

1872 else: 

1873 self.colorRangeWithTag( 

1874 s, i, j2, kind, delegate=None, exclude_match=exclude_match) 

1875 j = j2 

1876 self.prev = (i, j, kind) 

1877 self.trace_match(kind, s, i, j) 

1878 # New in Leo 5.5: don't recolor everything after continued strings. 

1879 if j > len(s) and not dots: 

1880 j = len(s) + 1 

1881 

1882 def span(s): 

1883 # Note: bindings are frozen by this def. 

1884 return self.restart_match_span(s, 

1885 # Positional args, in alpha order 

1886 delegate, end, exclude_match, kind, 

1887 no_escape, no_line_break, no_word_break) 

1888 

1889 self.setRestart(span, 

1890 # These must be keyword args. 

1891 delegate=delegate, end=end, 

1892 exclude_match=exclude_match, 

1893 kind=kind, 

1894 no_escape=no_escape, 

1895 no_line_break=no_line_break, 

1896 no_word_break=no_word_break) 

1897 return j - i # Correct, whatever j is. 

1898 #@+node:ekr.20110605121601.18623: *5* jedit.match_span_helper 

1899 def match_span_helper(self, s, i, pattern, no_escape, no_line_break, no_word_break): 

1900 """ 

1901 Return n >= 0 if s[i] ends with a non-escaped 'end' string. 

1902 """ 

1903 esc = self.escape 

1904 # pylint: disable=inconsistent-return-statements 

1905 while 1: 

1906 j = s.find(pattern, i) 

1907 if j == -1: 

1908 # Match to end of text if not found and no_line_break is False 

1909 if no_line_break: 

1910 return -1 

1911 return len(s) + 1 

1912 if no_word_break and j > 0 and s[j - 1] in self.word_chars: 

1913 return -1 # New in Leo 4.5. 

1914 if no_line_break and '\n' in s[i:j]: 

1915 return -1 

1916 if esc and not no_escape: 

1917 # Only an odd number of escapes is a 'real' escape. 

1918 escapes = 0 

1919 k = 1 

1920 while j - k >= 0 and s[j - k] == esc: 

1921 escapes += 1 

1922 k += 1 

1923 if (escapes % 2) == 1: 

1924 assert s[j - 1] == esc 

1925 i += 1 # 2013/08/26: just advance past the *one* escaped character. 

1926 else: 

1927 return j 

1928 else: 

1929 return j 

1930 # For pylint. 

1931 return -1 

1932 #@+node:ekr.20110605121601.18624: *5* jedit.restart_match_span 

1933 def restart_match_span(self, s, 

1934 delegate, end, exclude_match, kind, 

1935 no_escape, no_line_break, no_word_break 

1936 ): 

1937 """Remain in this state until 'end' is seen.""" 

1938 self.matcher_name = 'restart:' + self.matcher_name.replace('restart:', '') 

1939 i = 0 

1940 j = self.match_span_helper(s, i, end, no_escape, no_line_break, no_word_break) 

1941 if j == -1: 

1942 j2 = len(s) + 1 

1943 elif j > len(s): 

1944 j2 = j 

1945 else: 

1946 j2 = j + len(end) 

1947 if delegate: 

1948 self.colorRangeWithTag(s, i, j, kind, 

1949 delegate=delegate, exclude_match=exclude_match) 

1950 self.colorRangeWithTag(s, j, j2, kind, 

1951 delegate=None, exclude_match=exclude_match) 

1952 else: # avoid having to merge ranges in addTagsToList. 

1953 self.colorRangeWithTag(s, i, j2, kind, 

1954 delegate=None, exclude_match=exclude_match) 

1955 j = j2 

1956 self.trace_match(kind, s, i, j) 

1957 if j > len(s): 

1958 

1959 def span(s): 

1960 return self.restart_match_span(s, 

1961 # Positional args, in alpha order 

1962 delegate, end, exclude_match, kind, 

1963 no_escape, no_line_break, no_word_break) 

1964 

1965 self.setRestart(span, 

1966 # These must be keywords args. 

1967 delegate=delegate, end=end, kind=kind, 

1968 no_escape=no_escape, 

1969 no_line_break=no_line_break, 

1970 no_word_break=no_word_break) 

1971 else: 

1972 self.clearState() 

1973 return j # Return the new i, *not* the length of the match. 

1974 #@+node:ekr.20110605121601.18625: *4* jedit.match_span_regexp 

1975 def match_span_regexp(self, s, i, 

1976 kind='', begin='', end='', 

1977 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1978 delegate='', exclude_match=False, 

1979 no_escape=False, no_line_break=False, no_word_break=False, 

1980 ): 

1981 """ 

1982 Succeed if s[i:] starts with 'begin' (a regular expression) and 

1983 contains a following 'end'. 

1984 """ 

1985 if at_line_start and i != 0 and s[i - 1] != '\n': 

1986 return 0 

1987 if at_whitespace_end and i != g.skip_ws(s, 0): 

1988 return 0 

1989 if at_word_start and i > 0 and s[i - 1] in self.word_chars: 

1990 return 0 # 7/5/2008 

1991 if ( 

1992 at_word_start 

1993 and i + len(begin) + 1 < len(s) 

1994 and s[i + len(begin)] in self.word_chars 

1995 ): 

1996 return 0 # 7/5/2008 

1997 n = self.match_regexp_helper(s, i, begin) 

1998 # We may have to allow $n here, in which case we must use a regex object? 

1999 if n > 0: 

2000 j = i + n 

2001 j2 = s.find(end, j) 

2002 if j2 == -1: 

2003 return 0 

2004 if self.escape and not no_escape: 

2005 # Only an odd number of escapes is a 'real' escape. 

2006 escapes = 0 

2007 k = 1 

2008 while j - k >= 0 and s[j - k] == self.escape: 

2009 escapes += 1 

2010 k += 1 

2011 if (escapes % 2) == 1: 

2012 # An escaped end **aborts the entire match**: 

2013 # there is no way to 'restart' the regex. 

2014 return 0 

2015 i2 = j2 - len(end) 

2016 if delegate: 

2017 self.colorRangeWithTag( 

2018 s, i, j, kind, delegate=None, exclude_match=exclude_match) 

2019 self.colorRangeWithTag( 

2020 s, j, i2, kind, delegate=delegate, exclude_match=False) 

2021 self.colorRangeWithTag( 

2022 s, i2, j2, kind, delegate=None, exclude_match=exclude_match) 

2023 else: # avoid having to merge ranges in addTagsToList. 

2024 self.colorRangeWithTag( 

2025 s, i, j2, kind, delegate=None, exclude_match=exclude_match) 

2026 self.prev = (i, j, kind) 

2027 self.trace_match(kind, s, i, j2) 

2028 return j2 - i 

2029 return 0 

2030 #@+node:ekr.20190623132338.1: *4* jedit.match_tex_backslash 

2031 ascii_letters = re.compile(r'[a-zA-Z]+') 

2032 

2033 def match_tex_backslash(self, s, i, kind): 

2034 """ 

2035 Match the tex s[i:]. 

2036 

2037 (Conventional) acro names are a backslashe followed by either: 

2038 1. One or more ascii letters, or 

2039 2. Exactly one character, of any kind. 

2040 """ 

2041 assert s[i] == '\\' 

2042 m = self.ascii_letters.match(s, i + 1) 

2043 if m: 

2044 n = len(m.group(0)) 

2045 j = i + n + 1 

2046 else: 

2047 # Colorize the backslash plus exactly one more character. 

2048 j = i + 2 

2049 self.colorRangeWithTag(s, i, j, kind, delegate='') 

2050 self.prev = (i, j, kind) 

2051 self.trace_match(kind, s, i, j) 

2052 return j - i 

2053 #@+node:ekr.20170205074106.1: *4* jedit.match_wiki_pattern 

2054 def match_wiki_pattern(self, s, i, pattern): 

2055 """Show or hide a regex pattern managed by the wikiview plugin.""" 

2056 m = pattern.match(s, i) 

2057 if m: 

2058 n = len(m.group(0)) 

2059 self.colorRangeWithTag(s, i, i + n, 'url') 

2060 return n 

2061 return 0 

2062 #@+node:ekr.20110605121601.18626: *4* jedit.match_word_and_regexp 

2063 def match_word_and_regexp(self, s, i, 

2064 kind1='', word='', 

2065 kind2='', pattern='', 

2066 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

2067 exclude_match=False 

2068 ): 

2069 """Succeed if s[i:] matches pattern.""" 

2070 if not self.allow_mark_prev: 

2071 return 0 

2072 if at_line_start and i != 0 and s[i - 1] != '\n': 

2073 return 0 

2074 if at_whitespace_end and i != g.skip_ws(s, 0): 

2075 return 0 

2076 if at_word_start and i > 0 and s[i - 1] in self.word_chars: 

2077 return 0 

2078 if ( 

2079 at_word_start 

2080 and i + len(word) + 1 < len(s) 

2081 and s[i + len(word)] in self.word_chars 

2082 ): 

2083 j = i 

2084 if not g.match(s, i, word): 

2085 return 0 

2086 j = i + len(word) 

2087 n = self.match_regexp_helper(s, j, pattern) 

2088 if n == 0: 

2089 return 0 

2090 self.colorRangeWithTag(s, i, j, kind1, exclude_match=exclude_match) 

2091 k = j + n 

2092 self.colorRangeWithTag(s, j, k, kind2, exclude_match=False) 

2093 self.prev = (j, k, kind2) 

2094 self.trace_match(kind1, s, i, j) 

2095 self.trace_match(kind2, s, j, k) 

2096 return k - i 

2097 #@+node:ekr.20110605121601.18627: *4* jedit.skip_line 

2098 def skip_line(self, s, i): 

2099 if self.escape: 

2100 escape = self.escape + '\n' 

2101 n = len(escape) 

2102 while i < len(s): 

2103 j = g.skip_line(s, i) 

2104 if not g.match(s, j - n, escape): 

2105 return j 

2106 i = j 

2107 return i 

2108 return g.skip_line(s, i) 

2109 # Include the newline so we don't get a flash at the end of the line. 

2110 #@+node:ekr.20110605121601.18628: *4* jedit.trace_match 

2111 def trace_match(self, kind, s, i, j): 

2112 

2113 if j != i and self.trace_match_flag: 

2114 g.trace(kind, i, j, g.callers(2), self.dump(s[i:j])) 

2115 #@+node:ekr.20110605121601.18629: *3* jedit.State methods 

2116 #@+node:ekr.20110605121601.18630: *4* jedit.clearState 

2117 def clearState(self): 

2118 """ 

2119 Create a *language-specific* default state. 

2120 This properly forces a full recoloring when @language changes. 

2121 """ 

2122 n = self.initialStateNumber 

2123 self.setState(n) 

2124 return n 

2125 #@+node:ekr.20110605121601.18631: *4* jedit.computeState 

2126 def computeState(self, f, keys): 

2127 """ 

2128 Compute the state name associated with f and all the keys. 

2129 Return a unique int n representing that state. 

2130 """ 

2131 # Abbreviate arg names. 

2132 d = { 

2133 'delegate': '=>', 

2134 'end': 'end', 

2135 'at_line_start': 'start', 

2136 'at_whitespace_end': 'ws-end', 

2137 'exclude_match': '!match', 

2138 'no_escape': '!esc', 

2139 'no_line_break': '!lbrk', 

2140 'no_word_break': '!wbrk', 

2141 } 

2142 result = [self.languageTag(self.language)] 

2143 if not self.rulesetName.endswith('_main'): 

2144 result.append(self.rulesetName) 

2145 if f: 

2146 result.append(f.__name__) 

2147 for key in sorted(keys): 

2148 keyVal = keys.get(key) 

2149 val = d.get(key) 

2150 if val is None: 

2151 val = keys.get(key) 

2152 result.append(f"{key}={val}") 

2153 elif keyVal is True: 

2154 result.append(f"{val}") 

2155 elif keyVal is False: 

2156 pass 

2157 elif keyVal not in (None, ''): 

2158 result.append(f"{key}={keyVal}") 

2159 state = ';'.join(result).lower() 

2160 table = ( 

2161 ('kind=', ''), 

2162 ('literal', 'lit'), 

2163 ('restart', '@'), 

2164 ) 

2165 for pattern, s in table: 

2166 state = state.replace(pattern, s) 

2167 n = self.stateNameToStateNumber(f, state) 

2168 return n 

2169 #@+node:ekr.20110605121601.18632: *4* jedit.getters & setters 

2170 def currentBlockNumber(self): 

2171 block = self.highlighter.currentBlock() 

2172 return block.blockNumber() if block and block.isValid() else -1 

2173 

2174 def currentState(self): 

2175 return self.highlighter.currentBlockState() 

2176 

2177 def prevState(self): 

2178 return self.highlighter.previousBlockState() 

2179 

2180 def setState(self, n): 

2181 self.highlighter.setCurrentBlockState(n) 

2182 return n 

2183 #@+node:ekr.20170125141148.1: *4* jedit.inColorState 

2184 def inColorState(self): 

2185 """True if the *current* state is enabled.""" 

2186 n = self.currentState() 

2187 state = self.stateDict.get(n, 'no-state') 

2188 enabled = ( 

2189 not state.endswith('@nocolor') and 

2190 not state.endswith('@nocolor-node') and 

2191 not state.endswith('@killcolor')) 

2192 return enabled 

2193 #@+node:ekr.20110605121601.18633: *4* jedit.setRestart 

2194 def setRestart(self, f, **keys): 

2195 n = self.computeState(f, keys) 

2196 self.setState(n) 

2197 return n 

2198 #@+node:ekr.20110605121601.18635: *4* jedit.show... 

2199 def showState(self, n): 

2200 state = self.stateDict.get(n, 'no-state') 

2201 return f"{n:2}:{state}" 

2202 

2203 def showCurrentState(self): 

2204 n = self.currentState() 

2205 return self.showState(n) 

2206 

2207 def showPrevState(self): 

2208 n = self.prevState() 

2209 return self.showState(n) 

2210 #@+node:ekr.20110605121601.18636: *4* jedit.stateNameToStateNumber 

2211 def stateNameToStateNumber(self, f, stateName): 

2212 """ 

2213 stateDict: Keys are state numbers, values state names. 

2214 stateNameDict: Keys are state names, values are state numbers. 

2215 restartDict: Keys are state numbers, values are restart functions 

2216 """ 

2217 n = self.stateNameDict.get(stateName) 

2218 if n is None: 

2219 n = self.nextState 

2220 self.stateNameDict[stateName] = n 

2221 self.stateDict[n] = stateName 

2222 self.restartDict[n] = f 

2223 self.nextState += 1 

2224 self.n2languageDict[n] = self.language 

2225 return n 

2226 #@+node:ekr.20110605121601.18637: *3* jedit.colorRangeWithTag 

2227 def colorRangeWithTag(self, s, i, j, tag, delegate='', exclude_match=False): 

2228 """ 

2229 Actually colorize the selected range. 

2230 

2231 This is called whenever a pattern matcher succeed. 

2232 """ 

2233 trace = 'coloring' in g.app.debug and not g.unitTesting 

2234 # setTag does most tracing. 

2235 if not self.inColorState(): 

2236 # Do *not* check x.flag here. It won't work. 

2237 if trace: 

2238 g.trace('not in color state') 

2239 return 

2240 self.delegate_name = delegate 

2241 if delegate: 

2242 if trace: 

2243 if len(repr(s[i:j])) <= 20: 

2244 s2 = repr(s[i:j]) 

2245 else: 

2246 s2 = repr(s[i : i + 17 - 2] + '...') 

2247 kind_s = f"{delegate}:{tag}" 

2248 print( 

2249 f"\ncolorRangeWithTag: {kind_s:25} {i:3} {j:3} " 

2250 f"{s2:>20} {self.matcher_name}\n") 

2251 self.modeStack.append(self.modeBunch) 

2252 self.init_mode(delegate) 

2253 while 0 <= i < j and i < len(s): 

2254 progress = i 

2255 assert j >= 0, j 

2256 for f in self.rulesDict.get(s[i], []): 

2257 n = f(self, s, i) 

2258 if n is None: 

2259 g.trace('Can not happen: delegate matcher returns None') 

2260 elif n > 0: 

2261 self.matcher_name = f.__name__ 

2262 i += n 

2263 break 

2264 else: 

2265 # Use the default chars for everything else. 

2266 # Use the *delegate's* default characters if possible. 

2267 default_tag = self.attributesDict.get('default') 

2268 self.setTag(default_tag or tag, s, i, i + 1) 

2269 i += 1 

2270 assert i > progress 

2271 bunch = self.modeStack.pop() 

2272 self.initModeFromBunch(bunch) 

2273 elif not exclude_match: 

2274 self.setTag(tag, s, i, j) 

2275 if tag != 'url': 

2276 # Allow UNL's and URL's *everywhere*. 

2277 j = min(j, len(s)) 

2278 while i < j: 

2279 ch = s[i].lower() 

2280 if ch == 'u': 

2281 n = self.match_unl(s, i) 

2282 i += max(1, n) 

2283 elif ch in 'fh': # file|ftp|http|https 

2284 n = self.match_any_url(s, i) 

2285 i += max(1, n) 

2286 else: 

2287 i += 1 

2288 #@+node:ekr.20110605121601.18638: *3* jedit.mainLoop 

2289 tot_time = 0.0 

2290 

2291 def mainLoop(self, n, s): 

2292 """Colorize a *single* line s, starting in state n.""" 

2293 trace = 'coloring' in g.app.debug 

2294 t1 = time.process_time() 

2295 f = self.restartDict.get(n) 

2296 if trace: 

2297 p = self.c and self.c.p 

2298 if p and p.v != self.last_v: 

2299 self.last_v = p.v 

2300 f_name = f.__name__ if f else 'None' 

2301 print('') 

2302 g.trace(f"NEW NODE: state {n} = {f_name} {p.h}\n") 

2303 i = f(s) if f else 0 

2304 while i < len(s): 

2305 progress = i 

2306 functions = self.rulesDict.get(s[i], []) 

2307 for f in functions: 

2308 n = f(self, s, i) 

2309 if n is None: 

2310 g.trace('Can not happen: n is None', repr(f)) 

2311 break 

2312 elif n > 0: # Success. The match has already been colored. 

2313 self.matcher_name = f.__name__ # For traces. 

2314 i += n 

2315 break 

2316 elif n < 0: # Total failure. 

2317 i += -n 

2318 break 

2319 else: # Partial failure: Do not break or change i! 

2320 pass 

2321 else: 

2322 i += 1 

2323 assert i > progress 

2324 # Don't even *think* about changing state here. 

2325 self.tot_time += time.process_time() - t1 

2326 #@+node:ekr.20110605121601.18640: *3* jedit.recolor & helpers 

2327 def recolor(self, s): 

2328 """ 

2329 jEdit.recolor: Recolor a *single* line, s. 

2330 QSyntaxHighligher calls this method repeatedly and automatically. 

2331 """ 

2332 p = self.c.p 

2333 self.recolorCount += 1 

2334 block_n = self.currentBlockNumber() 

2335 n = self.prevState() 

2336 if p.v == self.old_v: 

2337 new_language = self.n2languageDict.get(n) 

2338 if new_language != self.language: 

2339 self.language = new_language 

2340 self.init(p) 

2341 else: 

2342 self.updateSyntaxColorer(p) # Force a full recolor 

2343 assert self.language 

2344 self.init_all_state(p.v) 

2345 self.init(p) 

2346 if block_n == 0: 

2347 n = self.initBlock0() 

2348 n = self.setState(n) # Required. 

2349 # Always color the line, even if colorizing is disabled. 

2350 if s: 

2351 self.mainLoop(n, s) 

2352 #@+node:ekr.20170126100139.1: *4* jedit.initBlock0 

2353 def initBlock0(self): 

2354 """ 

2355 Init *local* ivars when handling block 0. 

2356 This prevents endless recalculation of the proper default state. 

2357 """ 

2358 if self.enabled: 

2359 n = self.setInitialStateNumber() 

2360 else: 

2361 n = self.setRestart(self.restartNoColor) 

2362 return n 

2363 #@+node:ekr.20170126101049.1: *4* jedit.setInitialStateNumber 

2364 def setInitialStateNumber(self): 

2365 """ 

2366 Init the initialStateNumber ivar for clearState() 

2367 This saves a lot of work. 

2368 

2369 Called from init() and initBlock0. 

2370 """ 

2371 state = self.languageTag(self.language) 

2372 n = self.stateNameToStateNumber(None, state) 

2373 self.initialStateNumber = n 

2374 self.blankStateNumber = self.stateNameToStateNumber(None, state + ';blank') 

2375 return n 

2376 #@+node:ekr.20170126103925.1: *4* jedit.languageTag 

2377 def languageTag(self, name): 

2378 """ 

2379 Return the standardized form of the language name. 

2380 Doing this consistently prevents subtle bugs. 

2381 """ 

2382 if name: 

2383 table = ( 

2384 ('markdown', 'md'), 

2385 ('python', 'py'), 

2386 ('javascript', 'js'), 

2387 ) 

2388 for pattern, s in table: 

2389 name = name.replace(pattern, s) 

2390 return name 

2391 return 'no-language' 

2392 #@+node:ekr.20170205055743.1: *3* jedit.set_wikiview_patterns 

2393 def set_wikiview_patterns(self, leadins, patterns): 

2394 """ 

2395 Init the colorizer so it will *skip* all patterns. 

2396 The wikiview plugin calls this method. 

2397 """ 

2398 d = self.rulesDict 

2399 for leadins_list, pattern in zip(leadins, patterns): 

2400 for ch in leadins_list: 

2401 

2402 def wiki_rule(self, s, i, pattern=pattern): 

2403 """Bind pattern and leadin for jedit.match_wiki_pattern.""" 

2404 return self.match_wiki_pattern(s, i, pattern) 

2405 

2406 aList = d.get(ch, []) 

2407 if wiki_rule not in aList: 

2408 aList.insert(0, wiki_rule) 

2409 d[ch] = aList 

2410 self.rulesDict = d 

2411 #@-others 

2412#@+node:ekr.20110605121601.18565: ** class LeoHighlighter (QSyntaxHighlighter) 

2413# Careful: we may be running from the bridge. 

2414 

2415if QtGui: 

2416 

2417 

2418 class LeoHighlighter(QtGui.QSyntaxHighlighter): # type:ignore 

2419 """ 

2420 A subclass of QSyntaxHighlighter that overrides 

2421 the highlightBlock and rehighlight methods. 

2422 

2423 All actual syntax coloring is done in the highlighter class. 

2424 

2425 Used by both the JeditColorizer and PYgmentsColorizer classes. 

2426 """ 

2427 # This is c.frame.body.colorizer.highlighter 

2428 #@+others 

2429 #@+node:ekr.20110605121601.18566: *3* leo_h.ctor (sets style) 

2430 def __init__(self, c, colorizer, document): 

2431 """ctor for LeoHighlighter class.""" 

2432 self.c = c 

2433 self.colorizer = colorizer 

2434 self.n_calls = 0 

2435 assert isinstance(document, QtGui.QTextDocument), document 

2436 # Alas, a QsciDocument is not a QTextDocument. 

2437 self.leo_document = document 

2438 super().__init__(document) 

2439 self.reloadSettings() 

2440 #@+node:ekr.20110605121601.18567: *3* leo_h.highlightBlock 

2441 def highlightBlock(self, s): 

2442 """ Called by QSyntaxHighlighter """ 

2443 self.n_calls += 1 

2444 s = g.toUnicode(s) 

2445 self.colorizer.recolor(s) 

2446 # Highlight just one line. 

2447 #@+node:ekr.20190327052228.1: *3* leo_h.reloadSettings 

2448 def reloadSettings(self): 

2449 """Reload all reloadable settings.""" 

2450 c, document = self.c, self.leo_document 

2451 if not pygments: 

2452 return 

2453 if not c.config.getBool('use-pygments', default=False): 

2454 return 

2455 # 

2456 # Init pygments ivars. 

2457 self._brushes = {} 

2458 self._document = document 

2459 self._formats = {} 

2460 self.colorizer.style_name = 'default' 

2461 style_name = c.config.getString('pygments-style-name') or 'default' 

2462 # Style gallery: https://help.farbox.com/pygments.html 

2463 # Dark styles: fruity, monokai, native, vim 

2464 # https://github.com/gthank/solarized-dark-pygments 

2465 if not c.config.getBool('use-pygments-styles', default=True): 

2466 return 

2467 # 

2468 # Init pygments style. 

2469 try: 

2470 self.setStyle(style_name) 

2471 # print('using %r pygments style in %r' % (style_name, c.shortFileName())) 

2472 except Exception: 

2473 print(f'pygments {style_name!r} style not found. Using "default" style') 

2474 self.setStyle('default') 

2475 style_name = 'default' 

2476 self.colorizer.style_name = style_name 

2477 assert self._style 

2478 #@+node:ekr.20190320154014.1: *3* leo_h: From PygmentsHighlighter 

2479 # 

2480 # All code in this tree is based on PygmentsHighlighter. 

2481 # 

2482 # Copyright (c) Jupyter Development Team. 

2483 # Distributed under the terms of the Modified BSD License. 

2484 #@+others 

2485 #@+node:ekr.20190320153605.1: *4* leo_h._get_format & helpers 

2486 def _get_format(self, token): 

2487 """ Returns a QTextCharFormat for token or None. 

2488 """ 

2489 if token in self._formats: 

2490 return self._formats[token] 

2491 if self._style is None: 

2492 result = self._get_format_from_document(token, self._document) 

2493 else: 

2494 result = self._get_format_from_style(token, self._style) 

2495 result = self._get_format_from_style(token, self._style) 

2496 self._formats[token] = result 

2497 return result 

2498 #@+node:ekr.20190320162831.1: *5* pyg_h._get_format_from_document 

2499 def _get_format_from_document(self, token, document): 

2500 """ Returns a QTextCharFormat for token by 

2501 """ 

2502 # Modified by EKR. 

2503 # These lines cause unbounded recursion. 

2504 # code, html = next(self._formatter._format_lines([(token, u'dummy')])) 

2505 # self._document.setHtml(html) 

2506 return QtGui.QTextCursor(self._document).charFormat() 

2507 #@+node:ekr.20190320153716.1: *5* leo_h._get_format_from_style 

2508 key_error_d: Dict[str, bool] = {} 

2509 

2510 def _get_format_from_style(self, token, style): 

2511 """ Returns a QTextCharFormat for token by reading a Pygments style. 

2512 """ 

2513 result = QtGui.QTextCharFormat() 

2514 # 

2515 # EKR: handle missing tokens. 

2516 try: 

2517 data = style.style_for_token(token).items() 

2518 except KeyError as err: 

2519 key = repr(err) 

2520 if key not in self.key_error_d: 

2521 self.key_error_d[key] = True 

2522 g.trace(err) 

2523 return result 

2524 for key, value in data: 

2525 if value: 

2526 if key == 'color': 

2527 result.setForeground(self._get_brush(value)) 

2528 elif key == 'bgcolor': 

2529 result.setBackground(self._get_brush(value)) 

2530 elif key == 'bold': 

2531 result.setFontWeight(Weight.Bold) 

2532 elif key == 'italic': 

2533 result.setFontItalic(True) 

2534 elif key == 'underline': 

2535 result.setUnderlineStyle(UnderlineStyle.SingleUnderline) 

2536 elif key == 'sans': 

2537 result.setFontStyleHint(Weight.SansSerif) 

2538 elif key == 'roman': 

2539 result.setFontStyleHint(Weight.Times) 

2540 elif key == 'mono': 

2541 result.setFontStyleHint(Weight.TypeWriter) 

2542 return result 

2543 #@+node:ekr.20190320153958.1: *4* leo_h.setStyle 

2544 def setStyle(self, style): 

2545 """ Sets the style to the specified Pygments style. 

2546 """ 

2547 from pygments.styles import get_style_by_name # type:ignore 

2548 

2549 if isinstance(style, str): 

2550 style = get_style_by_name(style) 

2551 self._style = style 

2552 self._clear_caches() 

2553 #@+node:ekr.20190320154604.1: *4* leo_h.clear_caches 

2554 def _clear_caches(self): 

2555 """ Clear caches for brushes and formats. 

2556 """ 

2557 self._brushes = {} 

2558 self._formats = {} 

2559 #@+node:ekr.20190320154752.1: *4* leo_h._get_brush/color 

2560 def _get_brush(self, color): 

2561 """ Returns a brush for the color. 

2562 """ 

2563 result = self._brushes.get(color) 

2564 if result is None: 

2565 qcolor = self._get_color(color) 

2566 result = QtGui.QBrush(qcolor) 

2567 self._brushes[color] = result 

2568 return result 

2569 

2570 def _get_color(self, color): 

2571 """ Returns a QColor built from a Pygments color string. 

2572 """ 

2573 qcolor = QtGui.QColor() 

2574 qcolor.setRgb(int(color[:2], base=16), 

2575 int(color[2:4], base=16), 

2576 int(color[4:6], base=16)) 

2577 return qcolor 

2578 #@-others 

2579 #@-others 

2580#@+node:ekr.20140906095826.18717: ** class NullScintillaLexer (QsciLexerCustom) 

2581if Qsci: 

2582 

2583 

2584 class NullScintillaLexer(Qsci.QsciLexerCustom): # type:ignore 

2585 """A do-nothing colorizer for Scintilla.""" 

2586 

2587 def __init__(self, c, parent=None): 

2588 super().__init__(parent) 

2589 # Init the pase class 

2590 self.leo_c = c 

2591 self.configure_lexer() 

2592 

2593 def description(self, style): 

2594 return 'NullScintillaLexer' 

2595 

2596 def setStyling(self, length, style): 

2597 g.trace('(NullScintillaLexer)', length, style) 

2598 

2599 def styleText(self, start, end): 

2600 """Style the text from start to end.""" 

2601 

2602 def configure_lexer(self): 

2603 """Configure the QScintilla lexer.""" 

2604 # c = self.leo_c 

2605 lexer = self 

2606 # To do: use c.config setting. 

2607 # pylint: disable=no-member 

2608 font = QtGui.QFont("DejaVu Sans Mono", 14) 

2609 lexer.setFont(font) 

2610#@+node:ekr.20190319151826.1: ** class PygmentsColorizer(BaseJEditColorizer) 

2611class PygmentsColorizer(BaseJEditColorizer): 

2612 """ 

2613 This class adapts pygments tokens to QSyntaxHighlighter. 

2614 """ 

2615 # This is c.frame.body.colorizer 

2616 #@+others 

2617 #@+node:ekr.20190319151826.3: *3* pyg_c.__init__ & helpers 

2618 def __init__(self, c, widget, wrapper): 

2619 """Ctor for JEditColorizer class.""" 

2620 super().__init__(c, widget, wrapper) 

2621 # 

2622 # Create the highlighter. The default is NullObject. 

2623 if isinstance(widget, QtWidgets.QTextEdit): 

2624 self.highlighter = LeoHighlighter(c, 

2625 colorizer=self, 

2626 document=widget.document(), 

2627 ) 

2628 # 

2629 # State unique to this class... 

2630 self.color_enabled = self.enabled 

2631 self.old_v = None 

2632 # 

2633 # Init common data... 

2634 # self.init_style_ivars() 

2635 # self.defineLeoKeywordsDict() 

2636 # self.defineDefaultColorsDict() 

2637 # self.defineDefaultFontDict() 

2638 self.reloadSettings() 

2639 # self.init() 

2640 #@+node:ekr.20190324043722.1: *4* pyg_c.init 

2641 def init(self, p=None): 

2642 """Init the colorizer. p is for tracing only.""" 

2643 # 

2644 # Like jedit.init, but no need to init state. 

2645 self.init_mode(self.language) 

2646 self.prev = None 

2647 # Used by setTag. 

2648 self.configure_tags() 

2649 

2650 def addLeoRules(self, theDict): 

2651 pass 

2652 #@+node:ekr.20190324051704.1: *4* pyg_c.reloadSettings 

2653 def reloadSettings(self): 

2654 """Reload the base settings, plus pygments settings.""" 

2655 if 'coloring' in g.app.debug and not g.unitTesting: 

2656 print('reloading pygments settings.') 

2657 # Do basic inits. 

2658 BaseJEditColorizer.reloadSettings(self) 

2659 # Bind methods. 

2660 if self.use_pygments_styles: 

2661 self.getDefaultFormat = QtGui.QTextCharFormat 

2662 self.getFormat = self.getPygmentsFormat 

2663 self.setFormat = self.setPygmentsFormat 

2664 else: 

2665 self.getDefaultFormat = self.getLegacyDefaultFormat 

2666 self.getFormat = self.getLegacyFormat 

2667 self.setFormat = self.setLegacyFormat 

2668 # Init everything else. 

2669 self.init_style_ivars() 

2670 self.defineLeoKeywordsDict() 

2671 self.defineDefaultColorsDict() 

2672 self.defineDefaultFontDict() 

2673 self.init() 

2674 #@+node:ekr.20190324063349.1: *3* pyg_c.format getters 

2675 def getLegacyDefaultFormat(self): 

2676 return None 

2677 

2678 traced_dict: Dict[str, str] = {} 

2679 

2680 def getLegacyFormat(self, token, text): 

2681 """Return a jEdit tag for the given pygments token.""" 

2682 r = repr(token).lstrip('Token.').lstrip('Literal.').lower() 

2683 # Tables and setTag assume lower-case. 

2684 if r == 'name': 

2685 # Avoid a colision with existing Leo tag. 

2686 r = 'name.pygments' 

2687 if 0: 

2688 if r not in self.traced_dict: 

2689 self.traced_dict[r] = r 

2690 g.trace(r) 

2691 return r 

2692 

2693 def getPygmentsFormat(self, token, text): 

2694 """Return a pygments format.""" 

2695 format = self.highlighter._formats.get(token) 

2696 if not format: 

2697 format = self.highlighter._get_format(token) 

2698 return format 

2699 #@+node:ekr.20190324064341.1: *3* pyg_c.format setters 

2700 def setLegacyFormat(self, index, length, format, s): 

2701 """Call the jEdit style setTag.""" 

2702 BaseJEditColorizer.setTag(self, format, s, index, index + length) 

2703 

2704 def setPygmentsFormat(self, index, length, format, s): 

2705 """Call the base setTag to set the Qt format.""" 

2706 self.highlighter.setFormat(index, length, format) 

2707 #@+node:ekr.20190319151826.78: *3* pyg_c.mainLoop & helpers 

2708 format_dict: Dict[str, str] = {} # Keys are repr(Token), values are formats. 

2709 lexers_dict: Dict[str, Callable] = {} # Keys are language names, values are instantiated, patched lexers. 

2710 state_s_dict: Dict[str, int] = {} # Keys are strings, values are ints. 

2711 state_n_dict: Dict[int, str] = {} # # Keys are ints, values are strings. 

2712 state_index = 1 # Index of state number to be allocated. 

2713 tot_time = 0.0 

2714 

2715 def mainLoop(self, s): 

2716 """Colorize a *single* line s""" 

2717 t1 = time.process_time() 

2718 highlighter = self.highlighter 

2719 # 

2720 # First, set the *expected* lexer. It may change later. 

2721 lexer = self.set_lexer() 

2722 # 

2723 # Restore the state. 

2724 # Based on Jupyter code: (c) Jupyter Development Team. 

2725 stack_ivar = '_saved_state_stack' 

2726 prev_data = highlighter.currentBlock().previous().userData() 

2727 if prev_data is not None: 

2728 # New code by EKR. Restore the language if necessary. 

2729 if self.language != prev_data.leo_language: 

2730 # Change the language and the lexer! 

2731 self.language = prev_data.leo_language 

2732 # g.trace('RESTORE:', self.language) 

2733 lexer = self.set_lexer() 

2734 setattr(lexer, stack_ivar, prev_data.syntax_stack) 

2735 elif hasattr(lexer, stack_ivar): 

2736 delattr(lexer, stack_ivar) 

2737 # g.trace(self.color_enabled, self.language, repr(s)) 

2738 # 

2739 # The main loop. Warning: this can change self.language. 

2740 index = 0 

2741 for token, text in lexer.get_tokens(s): 

2742 length = len(text) 

2743 # print('%5s %25r %r' % (self.color_enabled, repr(token).lstrip('Token.'), text)) 

2744 if self.color_enabled: 

2745 format = self.getFormat(token, text) 

2746 else: 

2747 format = self.getDefaultFormat() 

2748 self.setFormat(index, length, format, s) 

2749 index += length 

2750 # 

2751 # Save the state. 

2752 # Based on Jupyter code: (c) Jupyter Development Team. 

2753 stack = getattr(lexer, stack_ivar, None) 

2754 if stack: 

2755 data = PygmentsBlockUserData(syntax_stack=stack, leo_language=self.language) 

2756 highlighter.currentBlock().setUserData(data) 

2757 # Clean up for the next go-round. 

2758 delattr(lexer, stack_ivar) 

2759 # 

2760 # New code by EKR: 

2761 # - Fixes a bug so multiline tokens work. 

2762 # - State supports Leo's color directives. 

2763 state_s = f"{self.language}; {self.color_enabled}: {stack!r}" 

2764 state_n = self.state_s_dict.get(state_s) 

2765 if state_n is None: 

2766 state_n = self.state_index 

2767 self.state_index += 1 

2768 self.state_s_dict[state_s] = state_n 

2769 self.state_n_dict[state_n] = state_s 

2770 highlighter.setCurrentBlockState(state_n) 

2771 self.tot_time += time.process_time() - t1 

2772 #@+node:ekr.20190323045655.1: *4* pyg_c.at_color_callback 

2773 def at_color_callback(self, lexer, match): 

2774 from pygments.token import Name, Text # type: ignore 

2775 kind = match.group(0) 

2776 self.color_enabled = kind == '@color' 

2777 if self.color_enabled: 

2778 yield match.start(), Name.Decorator, kind 

2779 else: 

2780 yield match.start(), Text, kind 

2781 #@+node:ekr.20190323045735.1: *4* pyg_c.at_language_callback 

2782 def at_language_callback(self, lexer, match): 

2783 from pygments.token import Name 

2784 language = match.group(2) 

2785 ok = self.init_mode(language) 

2786 if ok: 

2787 self.language = language 

2788 yield match.start(), Name.Decorator, match.group(0) 

2789 else: 

2790 yield match.start(), Name.Decorator, match.group(1) 

2791 # Color only the @language, indicating an unknown language. 

2792 #@+node:ekr.20190322082533.1: *4* pyg_c.get_lexer 

2793 def get_lexer(self, language): 

2794 """Return the lexer for self.language, creating it if necessary.""" 

2795 import pygments.lexers as lexers # type: ignore 

2796 tag = 'get_lexer' 

2797 trace = 'coloring' in g.app.debug 

2798 try: 

2799 # #1520: always define lexer_language. 

2800 lexer_name = 'python3' if language == 'python' else language 

2801 lexer = lexers.get_lexer_by_name(lexer_name) 

2802 except Exception: 

2803 # pylint: disable=no-member 

2804 # One of the lexer's will not exist. 

2805 if trace: 

2806 g.trace(f"{tag}: no lexer for {language!r}") 

2807 lexer = lexers.Python3Lexer() 

2808 if trace and 'python' not in self.lexers_dict: 

2809 g.trace(f"{tag}: default lexer for python: {lexer!r}") 

2810 return lexer 

2811 #@+node:ekr.20190322094034.1: *4* pyg_c.patch_lexer 

2812 def patch_lexer(self, language, lexer): 

2813 

2814 from pygments.token import Comment # type:ignore 

2815 from pygments.lexer import inherit # type:ignore 

2816 

2817 

2818 class PatchedLexer(lexer.__class__): # type:ignore 

2819 

2820 leo_sec_ref_pat = r'(?-m:\<\<(.*?)\>\>)' 

2821 tokens = { 

2822 'root': [ 

2823 (r'^@(color|nocolor|killcolor)\b', self.at_color_callback), 

2824 (r'^(@language)\s+(\w+)', self.at_language_callback), 

2825 (leo_sec_ref_pat, self.section_ref_callback), 

2826 # Single-line, non-greedy match. 

2827 (r'(^\s*@doc|@)(\s+|\n)(.|\n)*?^@c', Comment.Leo.DocPart), 

2828 # Multi-line, non-greedy match. 

2829 inherit, 

2830 ], 

2831 } 

2832 

2833 try: 

2834 return PatchedLexer() 

2835 except Exception: 

2836 g.trace(f"can not patch {language!r}") 

2837 g.es_exception() 

2838 return lexer 

2839 #@+node:ekr.20190322133358.1: *4* pyg_c.section_ref_callback 

2840 def section_ref_callback(self, lexer, match): 

2841 """pygments callback for section references.""" 

2842 c = self.c 

2843 from pygments.token import Comment, Name 

2844 name, ref, start = match.group(1), match.group(0), match.start() 

2845 found = g.findReference(ref, c.p) 

2846 found_tok = Name.Entity if found else Name.Other 

2847 yield match.start(), Comment, '<<' 

2848 yield start + 2, found_tok, name 

2849 yield start + 2 + len(name), Comment, '>>' 

2850 #@+node:ekr.20190323064820.1: *4* pyg_c.set_lexer 

2851 def set_lexer(self): 

2852 """Return the lexer for self.language.""" 

2853 if self.language == 'patch': 

2854 self.language = 'diff' 

2855 key = f"{self.language}:{id(self)}" 

2856 lexer = self.lexers_dict.get(key) 

2857 if not lexer: 

2858 lexer = self.get_lexer(self.language) 

2859 lexer = self.patch_lexer(self.language, lexer) 

2860 self.lexers_dict[key] = lexer 

2861 return lexer 

2862 #@+node:ekr.20190319151826.79: *3* pyg_c.recolor 

2863 def recolor(self, s): 

2864 """ 

2865 PygmentsColorizer.recolor: Recolor a *single* line, s. 

2866 QSyntaxHighligher calls this method repeatedly and automatically. 

2867 """ 

2868 p = self.c.p 

2869 self.recolorCount += 1 

2870 if p.v != self.old_v: 

2871 self.updateSyntaxColorer(p) 

2872 # Force a full recolor 

2873 # sets self.language and self.enabled. 

2874 self.color_enabled = self.enabled 

2875 self.old_v = p.v 

2876 # Fix a major performance bug. 

2877 self.init(p) 

2878 # Support 

2879 assert self.language 

2880 if s is not None: 

2881 # For pygments, we *must* call for all lines. 

2882 self.mainLoop(s) 

2883 #@-others 

2884#@+node:ekr.20140906081909.18689: ** class QScintillaColorizer(BaseColorizer) 

2885# This is c.frame.body.colorizer 

2886 

2887 

2888class QScintillaColorizer(BaseColorizer): 

2889 """A colorizer for a QsciScintilla widget.""" 

2890 #@+others 

2891 #@+node:ekr.20140906081909.18709: *3* qsc.__init__ & reloadSettings 

2892 def __init__(self, c, widget, wrapper): 

2893 """Ctor for QScintillaColorizer. widget is a """ 

2894 super().__init__(c) 

2895 self.count = 0 # For unit testing. 

2896 self.colorCacheFlag = False 

2897 self.error = False # Set if there is an error in jeditColorizer.recolor 

2898 self.flag = True # Per-node enable/disable flag. 

2899 self.full_recolor_count = 0 # For unit testing. 

2900 self.language = 'python' # set by scanLanguageDirectives. 

2901 self.highlighter = None 

2902 self.lexer = None # Set in changeLexer. 

2903 widget.leo_colorizer = self 

2904 # Define/configure various lexers. 

2905 self.reloadSettings() 

2906 if Qsci: 

2907 self.lexersDict = self.makeLexersDict() 

2908 self.nullLexer = NullScintillaLexer(c) 

2909 else: 

2910 self.lexersDict = {} # type:ignore 

2911 self.nullLexer = g.NullObject() # type:ignore 

2912 

2913 def reloadSettings(self): 

2914 c = self.c 

2915 self.enabled = c.config.getBool('use-syntax-coloring') 

2916 #@+node:ekr.20170128141158.1: *3* qsc.scanColorDirectives (over-ride) 

2917 def scanColorDirectives(self, p): 

2918 """ 

2919 Return language based on the directives in p's ancestors. 

2920 Same as BaseColorizer.scanColorDirectives, except it also scans p.b. 

2921 """ 

2922 c = self.c 

2923 root = p.copy() 

2924 for p in root.self_and_parents(copy=False): 

2925 language = g.findFirstValidAtLanguageDirective(p.b) 

2926 if language: 

2927 return language 

2928 # Get the language from the nearest ancestor @<file> node. 

2929 language = g.getLanguageFromAncestorAtFileNode(root) or c.target_language 

2930 return language 

2931 #@+node:ekr.20140906081909.18718: *3* qsc.changeLexer 

2932 def changeLexer(self, language): 

2933 """Set the lexer for the given language.""" 

2934 c = self.c 

2935 wrapper = c.frame.body.wrapper 

2936 w = wrapper.widget # A Qsci.QsciSintilla object. 

2937 self.lexer = self.lexersDict.get(language, self.nullLexer) # type:ignore 

2938 w.setLexer(self.lexer) 

2939 #@+node:ekr.20140906081909.18707: *3* qsc.colorize 

2940 def colorize(self, p): 

2941 """The main Scintilla colorizer entry point.""" 

2942 # It would be much better to use QSyntaxHighlighter. 

2943 # Alas, a QSciDocument is not a QTextDocument. 

2944 self.updateSyntaxColorer(p) 

2945 self.changeLexer(self.language) 

2946 # if self.NEW: 

2947 # # Works, but QScintillaWrapper.tag_configuration is presently a do-nothing. 

2948 # for s in g.splitLines(p.b): 

2949 # self.jeditColorizer.recolor(s) 

2950 #@+node:ekr.20140906095826.18721: *3* qsc.configure_lexer 

2951 def configure_lexer(self, lexer): 

2952 """Configure the QScintilla lexer using @data qt-scintilla-styles.""" 

2953 c = self.c 

2954 qcolor, qfont = QtGui.QColor, QtGui.QFont 

2955 font = qfont("DejaVu Sans Mono", 14) 

2956 lexer.setFont(font) 

2957 lexer.setEolFill(False, -1) 

2958 if hasattr(lexer, 'setStringsOverNewlineAllowed'): 

2959 lexer.setStringsOverNewlineAllowed(False) 

2960 table: List[Tuple[str, str]] = [] 

2961 aList = c.config.getData('qt-scintilla-styles') 

2962 if aList: 

2963 aList = [s.split(',') for s in aList] 

2964 for z in aList: 

2965 if len(z) == 2: 

2966 color, style = z 

2967 table.append((color.strip(), style.strip()),) 

2968 else: g.trace(f"entry: {z}") 

2969 if not table: 

2970 black = '#000000' 

2971 firebrick3 = '#CD2626' 

2972 leo_green = '#00aa00' 

2973 # See http://pyqt.sourceforge.net/Docs/QScintilla2/classQsciLexerPython.html 

2974 # for list of selector names. 

2975 table = [ 

2976 # EKR's personal settings are reasonable defaults. 

2977 (black, 'ClassName'), 

2978 (firebrick3, 'Comment'), 

2979 (leo_green, 'Decorator'), 

2980 (leo_green, 'DoubleQuotedString'), 

2981 (black, 'FunctionMethodName'), 

2982 ('blue', 'Keyword'), 

2983 (black, 'Number'), 

2984 (leo_green, 'SingleQuotedString'), 

2985 (leo_green, 'TripleSingleQuotedString'), 

2986 (leo_green, 'TripleDoubleQuotedString'), 

2987 (leo_green, 'UnclosedString'), 

2988 # End of line where string is not closed 

2989 # style.python.13=fore:#000000,$(font.monospace),back:#E0C0E0,eolfilled 

2990 ] 

2991 for color, style in table: 

2992 if hasattr(lexer, style): 

2993 style_number = getattr(lexer, style) 

2994 try: 

2995 lexer.setColor(qcolor(color), style_number) 

2996 except Exception: 

2997 g.trace('bad color', color) 

2998 else: 

2999 pass 

3000 # Not an error. Not all lexers have all styles. 

3001 # g.trace('bad style: %s.%s' % (lexer.__class__.__name__, style)) 

3002 #@+node:ekr.20170128031840.1: *3* qsc.init 

3003 def init(self, p): 

3004 """QScintillaColorizer.init""" 

3005 self.updateSyntaxColorer(p) 

3006 self.changeLexer(self.language) 

3007 #@+node:ekr.20170128133525.1: *3* qsc.makeLexersDict 

3008 def makeLexersDict(self): 

3009 """Make a dictionary of Scintilla lexers, and configure each one.""" 

3010 c = self.c 

3011 # g.printList(sorted(dir(Qsci))) 

3012 parent = c.frame.body.wrapper.widget 

3013 table = ( 

3014 # 'Asm', 'Erlang', 'Forth', 'Haskell', 

3015 # 'LaTeX', 'Lisp', 'Markdown', 'Nsis', 'R', 

3016 'Bash', 'Batch', 'CPP', 'CSS', 'CMake', 'CSharp', 'CoffeeScript', 

3017 'D', 'Diff', 'Fortran', 'Fortran77', 'HTML', 

3018 'Java', 'JavaScript', 'Lua', 'Makefile', 'Matlab', 

3019 'Pascal', 'Perl', 'Python', 'PostScript', 'Properties', 

3020 'Ruby', 'SQL', 'TCL', 'TeX', 'XML', 'YAML', 

3021 ) 

3022 d = {} 

3023 for language_name in table: 

3024 class_name = 'QsciLexer' + language_name 

3025 lexer_class = getattr(Qsci, class_name, None) 

3026 if lexer_class: 

3027 # pylint: disable=not-callable 

3028 lexer = lexer_class(parent=parent) 

3029 self.configure_lexer(lexer) 

3030 d[language_name.lower()] = lexer 

3031 elif 0: 

3032 g.trace('no lexer for', class_name) 

3033 return d 

3034 #@-others 

3035#@+node:ekr.20190320062618.1: ** Jupyter classes 

3036# Copyright (c) Jupyter Development Team. 

3037# Distributed under the terms of the Modified BSD License. 

3038 

3039if pygments: 

3040 #@+others 

3041 #@+node:ekr.20190320062624.2: *3* RegexLexer.get_tokens_unprocessed 

3042 # Copyright (c) Jupyter Development Team. 

3043 # Distributed under the terms of the Modified BSD License. 

3044 

3045 from pygments.lexer import RegexLexer, _TokenType, Text, Error 

3046 

3047 def get_tokens_unprocessed(self, text, stack=('root',)): 

3048 """ 

3049 Split ``text`` into (tokentype, text) pairs. 

3050 

3051 Monkeypatched to store the final stack on the object itself. 

3052 

3053 The `text` parameter this gets passed is only the current line, so to 

3054 highlight things like multiline strings correctly, we need to retrieve 

3055 the state from the previous line (this is done in PygmentsHighlighter, 

3056 below), and use it to continue processing the current line. 

3057 """ 

3058 pos = 0 

3059 tokendefs = self._tokens 

3060 if hasattr(self, '_saved_state_stack'): 

3061 statestack = list(self._saved_state_stack) 

3062 else: 

3063 statestack = list(stack) 

3064 # Fix #1113... 

3065 try: 

3066 statetokens = tokendefs[statestack[-1]] 

3067 except Exception: 

3068 # g.es_exception() 

3069 return 

3070 while 1: 

3071 for rexmatch, action, new_state in statetokens: 

3072 m = rexmatch(text, pos) 

3073 if m: 

3074 if action is not None: 

3075 # pylint: disable=unidiomatic-typecheck 

3076 # EKR: Why not use isinstance? 

3077 if type(action) is _TokenType: 

3078 yield pos, action, m.group() 

3079 else: 

3080 for item in action(self, m): 

3081 yield item 

3082 pos = m.end() 

3083 if new_state is not None: 

3084 # state transition 

3085 if isinstance(new_state, tuple): 

3086 for state in new_state: 

3087 if state == '#pop': 

3088 statestack.pop() 

3089 elif state == '#push': 

3090 statestack.append(statestack[-1]) 

3091 else: 

3092 statestack.append(state) 

3093 elif isinstance(new_state, int): 

3094 # pop 

3095 del statestack[new_state:] 

3096 elif new_state == '#push': 

3097 statestack.append(statestack[-1]) 

3098 else: 

3099 assert False, f"wrong state def: {new_state!r}" 

3100 statetokens = tokendefs[statestack[-1]] 

3101 break 

3102 else: 

3103 try: 

3104 if text[pos] == '\n': 

3105 # at EOL, reset state to "root" 

3106 pos += 1 

3107 statestack = ['root'] 

3108 statetokens = tokendefs['root'] 

3109 yield pos, Text, '\n' 

3110 continue 

3111 yield pos, Error, text[pos] 

3112 pos += 1 

3113 except IndexError: 

3114 break 

3115 self._saved_state_stack = list(statestack) 

3116 

3117 # Monkeypatch! 

3118 

3119 if pygments: 

3120 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed 

3121 #@+node:ekr.20190320062624.3: *3* class PygmentsBlockUserData(QTextBlockUserData) 

3122 # Copyright (c) Jupyter Development Team. 

3123 # Distributed under the terms of the Modified BSD License. 

3124 

3125 if QtGui: 

3126 

3127 

3128 class PygmentsBlockUserData(QtGui.QTextBlockUserData): # type:ignore 

3129 """ Storage for the user data associated with each line.""" 

3130 

3131 syntax_stack = ('root',) 

3132 

3133 def __init__(self, **kwds): 

3134 for key, value in kwds.items(): 

3135 setattr(self, key, value) 

3136 super().__init__() 

3137 

3138 def __repr__(self): 

3139 attrs = ['syntax_stack'] 

3140 kwds = ', '.join([ 

3141 f"{attr}={getattr(self, attr)!r}" 

3142 for attr in attrs 

3143 ]) 

3144 return f"PygmentsBlockUserData({kwds})" 

3145 #@-others 

3146#@-others 

3147#@@language python 

3148#@@tabwidth -4 

3149#@@pagewidth 70 

3150#@-leo