Coverage for C:\leo.repo\leo-editor\leo\core\leoColorizer.py: 29%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1746 statements  

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.20220317050513.1: *3* BaseColorizer: birth 

49 #@+node:ekr.20190324044744.1: *4* BaseColorizer.__init__ 

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

51 """ctor for BaseColorizer class.""" 

52 # 

53 # Copy args... 

54 self.c = c 

55 self.widget = widget 

56 if widget: # #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 # Per-node enable/disable flag set by updateSyntaxColorer. 

64 self.highlighter = g.NullObject() # May be overridden in subclass... 

65 self.language = 'python' # set by scanLanguageDirectives. 

66 self.prev = None # Used by setTag. 

67 self.showInvisibles = False 

68 # 

69 # Statistics.... 

70 self.count = 0 

71 self.full_recolor_count = 0 # For unit tests. 

72 self.recolorCount = 0 

73 # 

74 # For traces... 

75 self.matcher_name = '' 

76 self.rulesetName = '' 

77 self.delegate_name = '' 

78 #@+node:ekr.20190324045134.1: *4* BaseColorizer.init 

79 def init(self): 

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

81 pass 

82 #@+node:ekr.20110605121601.18578: *4* BaseColorizer.configureTags & helpers 

83 def configureTags(self): 

84 """Configure all tags.""" 

85 wrapper = self.wrapper 

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

87 wrapper.start_tag_configure() 

88 self.configure_fonts() 

89 self.configure_colors() 

90 self.configure_variable_tags() 

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

92 wrapper.end_tag_configure() 

93 #@+node:ekr.20190324172632.1: *5* BaseColorizer.configure_colors 

94 def configure_colors(self): 

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

96 c, wrapper = self.c, self.wrapper 

97 # getColor puts the color name in standard form: 

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

99 getColor = c.config.getColor 

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

101 option_name, default_color = self.default_colors_dict[key] 

102 color = ( 

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

104 getColor(option_name) or 

105 default_color 

106 ) 

107 # Must use foreground, not fg. 

108 try: 

109 wrapper.tag_configure(key, foreground=color) 

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

111 g.es_exception() 

112 wrapper.tag_configure(key, foreground=default_color) 

113 #@+node:ekr.20190324172242.1: *5* BaseColorizer.configure_fonts & helper 

114 def configure_fonts(self): 

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

116 c = self.c 

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

118 wrapper = self.wrapper 

119 # 

120 # Get the default body font. 

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

122 if not defaultBodyfont: 

123 defaultBodyfont = c.config.getFontFromParams( 

124 "body_text_font_family", "body_text_font_size", 

125 "body_text_font_slant", "body_text_font_weight", 

126 c.config.defaultBodyFontSize) 

127 self.fonts['default_body_font'] = defaultBodyfont 

128 # 

129 # Set all fonts. 

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

131 option_name = self.default_font_dict[key] 

132 # Find language specific setting before general setting. 

133 table = ( 

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

135 option_name, 

136 ) 

137 for name in table: 

138 font = self.fonts.get(name) 

139 if font: 

140 break 

141 font = self.find_font(key, name) 

142 if font: 

143 self.fonts[key] = font 

144 wrapper.tag_configure(key, font=font) 

145 if isQt and key == 'url': 

146 font.setUnderline(True) 

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

148 self.configure_hard_tab_width(font) 

149 break 

150 else: 

151 # Neither setting exists. 

152 self.fonts[key] = None # Essential 

153 wrapper.tag_configure(key, font=defaultBodyfont) 

154 #@+node:ekr.20190326034006.1: *6* BaseColorizer.find_font 

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

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

157 

158 def find_font(self, key, setting_name): 

159 """ 

160 Return the font for the given setting name. 

161 """ 

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

163 c, get = self.c, self.c.config.get 

164 default_size = c.config.defaultBodyFontSize 

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

166 size_error = False 

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

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

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

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

171 if family or slant or weight or size: 

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

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

174 if key in self.zoom_dict: 

175 old_size = self.zoom_dict.get(key) 

176 else: 

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

178 old_size = size or default_size 

179 if isinstance(old_size, str): 

180 # All settings should be in units of points. 

181 try: 

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

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

184 else: 

185 old_size = int(old_size) 

186 except ValueError: 

187 size_error = True 

188 elif not isinstance(old_size, int): 

189 size_error = True 

190 if size_error: 

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

192 size = old_size 

193 else: 

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

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

196 if zoom_delta: 

197 size = old_size + zoom_delta 

198 self.zoom_dict[key] = size 

199 slant = slant or 'roman' 

200 weight = weight or 'normal' 

201 size = str(size) 

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

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

204 if font: 

205 if trace: 

206 g.trace( 

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

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

209 return font 

210 return None 

211 #@+node:ekr.20111024091133.16702: *5* BaseColorizer.configure_hard_tab_width 

212 def configure_hard_tab_width(self, font): 

213 """ 

214 Set the width of a hard tab. 

215 

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

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

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

219 

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

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

222 """ 

223 c, widget = self.c, self.widget 

224 if isinstance(widget, QtWidgets.QTextEdit): 

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

226 fm = QtGui.QFontMetrics(font) 

227 try: # fm.horizontalAdvance 

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

229 widget.setTabStopDistance(width) 

230 except Exception: 

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

232 widget.setTabStopWidth(width) # Obsolete. 

233 else: 

234 # To do: configure the QScintilla widget. 

235 pass 

236 #@+node:ekr.20110605121601.18579: *5* BaseColorizer.configure_variable_tags 

237 def configure_variable_tags(self): 

238 c = self.c 

239 wrapper = self.wrapper 

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

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

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

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

244 for name, option_name, default_color in ( 

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

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

247 ("elide", None, "yellow"), 

248 ): 

249 if self.showInvisibles: 

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

251 else: 

252 option_name, default_color = self.default_colors_dict.get( 

253 name, (None, None),) 

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

255 try: 

256 wrapper.tag_configure(name, background=color) 

257 except Exception: # A user error. 

258 wrapper.tag_configure(name, background=default_color) 

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

260 # Special case: 

261 if not self.showInvisibles: 

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

263 #@+node:ekr.20110605121601.18574: *4* BaseColorizer.defineDefaultColorsDict 

264 #@@nobeautify 

265 

266 def defineDefaultColorsDict (self): 

267 

268 # These defaults are sure to exist. 

269 self.default_colors_dict = { 

270 # 

271 # Used in Leo rules... 

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

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

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

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

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

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

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

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

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

281 # 

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

283 # 

284 # Top-level... 

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

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

287 'other' :('other', 'white'), 

288 'punctuation' :('punctuation', 'white'), 

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

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

291 # 

292 # Comment... 

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

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

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

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

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

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

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

300 # 

301 # Generic... 

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

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

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

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

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

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

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

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

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

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

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

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

314 # 

315 # Keyword... 

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

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

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

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

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

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

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

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

324 # 

325 # Literal... 

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

327 'literal' :('literal', 'white'), 

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

329 # 

330 # Name... 

331 # tag name :( option name, default color 

332 # 'name' defined below. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

354 # 

355 # Number... 

356 # tag name :( option name, default color 

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

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

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

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

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

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

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

364 # 

365 # Operator... 

366 # tag name :( option name, default color 

367 # 'operator' defined below. 

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

369 # 

370 # String... 

371 # tag name :( option name, default color 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

386 # 

387 # jEdit tags. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

408 } 

409 #@+node:ekr.20110605121601.18575: *4* BaseColorizer.defineDefaultFontDict 

410 #@@nobeautify 

411 

412 def defineDefaultFontDict (self): 

413 

414 self.default_font_dict = { 

415 # 

416 # Used in Leo rules... 

417 # tag name : option name 

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

419 'docpart' :'doc_part_font', 

420 'leokeyword' :'leo_keyword_font', 

421 'link' :'section_name_font', 

422 'name' :'undefined_section_name_font', 

423 'namebrackets' :'section_name_brackets_font', 

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

425 'url' :'url_font', 

426 # 

427 # Pygments tags (lower case)... 

428 # tag name : option name 

429 "comment" :'comment1_font', 

430 "comment.preproc" :'comment2_font', 

431 "comment.single" :'comment1_font', 

432 "error" :'null_font', 

433 "generic.deleted" :'literal4_font', 

434 "generic.emph" :'literal4_font', 

435 "generic.error" :'literal4_font', 

436 "generic.heading" :'literal4_font', 

437 "generic.inserted" :'literal4_font', 

438 "generic.output" :'literal4_font', 

439 "generic.prompt" :'literal4_font', 

440 "generic.strong" :'literal4_font', 

441 "generic.subheading":'literal4_font', 

442 "generic.traceback" :'literal4_font', 

443 "keyword" :'keyword1_font', 

444 "keyword.pseudo" :'keyword2_font', 

445 "keyword.type" :'keyword3_font', 

446 "name.attribute" :'null_font', 

447 "name.builtin" :'null_font', 

448 "name.class" :'null_font', 

449 "name.constant" :'null_font', 

450 "name.decorator" :'null_font', 

451 "name.entity" :'null_font', 

452 "name.exception" :'null_font', 

453 "name.function" :'null_font', 

454 "name.label" :'null_font', 

455 "name.namespace" :'null_font', 

456 "name.tag" :'null_font', 

457 "name.variable" :'null_font', 

458 "number" :'null_font', 

459 "operator.word" :'keyword4_font', 

460 "string" :'literal1_font', 

461 "string.doc" :'literal1_font', 

462 "string.escape" :'literal1_font', 

463 "string.interpol" :'literal1_font', 

464 "string.other" :'literal1_font', 

465 "string.regex" :'literal1_font', 

466 "string.single" :'literal1_font', 

467 "string.symbol" :'literal1_font', 

468 'xt' :'text_font', 

469 "whitespace" :'text_font', 

470 # 

471 # jEdit tags. 

472 # tag name : option name 

473 'comment1' :'comment1_font', 

474 'comment2' :'comment2_font', 

475 'comment3' :'comment3_font', 

476 'comment4' :'comment4_font', 

477 #'default' :'default_font', 

478 'function' :'function_font', 

479 'keyword1' :'keyword1_font', 

480 'keyword2' :'keyword2_font', 

481 'keyword3' :'keyword3_font', 

482 'keyword4' :'keyword4_font', 

483 'keyword5' :'keyword5_font', 

484 'label' :'label_font', 

485 'literal1' :'literal1_font', 

486 'literal2' :'literal2_font', 

487 'literal3' :'literal3_font', 

488 'literal4' :'literal4_font', 

489 'markup' :'markup_font', 

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

491 'null' :'null_font', 

492 'operator' :'operator_font', 

493 'trailing_whitespace' :'trailing_whitespace_font', 

494 } 

495 #@+node:ekr.20110605121601.18573: *4* BaseColorizer.defineLeoKeywordsDict 

496 def defineLeoKeywordsDict(self): 

497 self.leoKeywordsDict = {} 

498 for key in g.globalDirectiveList: 

499 self.leoKeywordsDict[key] = 'leokeyword' 

500 #@+node:ekr.20171114041307.1: *3* BaseColorizer.reloadSettings 

501 #@@nobeautify 

502 def reloadSettings(self): 

503 c, getBool = self.c, self.c.config.getBool 

504 # 

505 # Init all settings ivars. 

506 self.color_tags_list = [] 

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

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

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

510 self.use_pygments = None # Set in report_changes. 

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

512 # 

513 # Report changes to pygments settings. 

514 self.report_changes() 

515 # 

516 # Init the default fonts. 

517 self.bold_font = c.config.getFontFromParams( 

518 "body_text_font_family", "body_text_font_size", 

519 "body_text_font_slant", "body_text_font_weight", 

520 c.config.defaultBodyFontSize) 

521 self.italic_font = c.config.getFontFromParams( 

522 "body_text_font_family", "body_text_font_size", 

523 "body_text_font_slant", "body_text_font_weight", 

524 c.config.defaultBodyFontSize) 

525 self.bolditalic_font = c.config.getFontFromParams( 

526 "body_text_font_family", "body_text_font_size", 

527 "body_text_font_slant", "body_text_font_weight", 

528 c.config.defaultBodyFontSize) 

529 # Init everything else. 

530 self.init_style_ivars() 

531 self.defineLeoKeywordsDict() 

532 self.defineDefaultColorsDict() 

533 self.defineDefaultFontDict() 

534 self.configureTags() 

535 self.init() 

536 #@+node:ekr.20190327053604.1: *4* BaseColorizer.report_changes 

537 prev_use_pygments = None 

538 prev_use_styles = None 

539 prev_style = None 

540 

541 def report_changes(self): 

542 """Report changes to pygments settings""" 

543 c = self.c 

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

545 if not use_pygments: # 1696. 

546 return 

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

548 if trace: 

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

550 

551 def show(setting, val): 

552 if trace: 

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

554 

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

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

557 if self.prev_use_pygments is None: 

558 self.use_pygments = self.prev_use_pygments = use_pygments 

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

560 elif use_pygments == self.prev_use_pygments: 

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

562 else: 

563 g.es_print( 

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

565 f"{self.prev_use_pygments}", 

566 color='red') 

567 # This setting is used only in the LeoHighlighter class 

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

569 # Report everything if we are tracing. 

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

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

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

573 if self.prev_use_styles is None: 

574 self.prev_use_styles = self.use_pygments_styles 

575 elif self.use_pygments_styles != self.prev_use_styles: 

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

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

578 if not self.use_pygments_styles: 

579 return 

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

581 if self.prev_style is None: 

582 self.prev_style = style_name 

583 elif style_name != self.prev_style: 

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

585 self.prev_style = style_name 

586 #@+node:ekr.20190324050727.1: *4* BaseColorizer.init_style_ivars 

587 def init_style_ivars(self): 

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

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

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

591 self.hyperCount = 0 

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

593 self.default = 'null' 

594 self.digit_re = '' 

595 self.escape = '' 

596 self.highlight_digits = True 

597 self.ignore_case = True 

598 self.no_word_sep = '' 

599 # Debugging... 

600 self.allow_mark_prev = True 

601 self.n_setTag = 0 

602 self.tagCount = 0 

603 self.trace_leo_matches = False 

604 self.trace_match_flag = False 

605 # Profiling... 

606 self.recolorCount = 0 # Total calls to recolor 

607 self.stateCount = 0 # Total calls to setCurrentState 

608 self.totalStates = 0 

609 self.maxStateNumber = 0 

610 self.totalKeywordsCalls = 0 

611 self.totalLeoKeywordsCalls = 0 

612 # Mode data... 

613 self.defaultRulesList = [] 

614 self.importedRulesets = {} 

615 self.initLanguage = None 

616 self.prev = None # The previous token. 

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

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

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

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

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

622 self.modeStack = [] 

623 self.rulesDict = {} 

624 # self.defineAndExtendForthWords() 

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

626 self.tags = [ 

627 # 8 Leo-specific tags. 

628 "blank", # show_invisibles_space_color 

629 "docpart", 

630 "leokeyword", 

631 "link", 

632 "name", 

633 "namebrackets", 

634 "tab", # show_invisibles_space_color 

635 "url", 

636 # jEdit tags. 

637 'comment1', 'comment2', 'comment3', 'comment4', 

638 # default, # exists, but never generated. 

639 'function', 

640 'keyword1', 'keyword2', 'keyword3', 'keyword4', 

641 'label', 'literal1', 'literal2', 'literal3', 'literal4', 

642 'markup', 'operator', 

643 'trailing_whitespace', 

644 ] 

645 #@+node:ekr.20110605121601.18641: *3* BaseColorizer.setTag 

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

647 """Set the tag in the highlighter.""" 

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

649 self.n_setTag += 1 

650 if i == j: 

651 return 

652 wrapper = self.wrapper # A QTextEditWrapper 

653 if not tag.strip(): 

654 return 

655 tag = tag.lower().strip() 

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

657 dots = tag.startswith('dots') 

658 if dots: 

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

660 colorName = wrapper.configDict.get(tag) # This color name should already be valid. 

661 if not colorName: 

662 return 

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

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

665 colorName = colorName.replace( 

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

667 colorName = leo_color_database.get(colorName, colorName) 

668 # Get the actual color. 

669 color = self.actualColorDict.get(colorName) 

670 if not color: 

671 color = QtGui.QColor(colorName) 

672 if color.isValid(): 

673 self.actualColorDict[colorName] = color 

674 else: 

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

676 return 

677 underline = wrapper.configUnderlineDict.get(tag) 

678 format = QtGui.QTextCharFormat() 

679 font = self.fonts.get(tag) 

680 if font: 

681 format.setFont(font) 

682 self.configure_hard_tab_width(font) # #1919. 

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

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

685 format.setFontUnderline(True) 

686 if colorName != 'black': 

687 format.setBackground(color) 

688 elif underline: 

689 format.setForeground(color) 

690 format.setUnderlineStyle(UnderlineStyle.SingleUnderline) 

691 format.setFontUnderline(True) 

692 elif dots or tag == 'trailing_whitespace': 

693 format.setForeground(color) 

694 format.setUnderlineStyle(UnderlineStyle.DotLine) 

695 else: 

696 format.setForeground(color) 

697 format.setUnderlineStyle(UnderlineStyle.NoUnderline) 

698 self.tagCount += 1 

699 if trace: 

700 # A superb trace. 

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

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

703 else: 

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

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

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

707 print( 

708 f"setTag: {kind_s:32} {i:3} {j:3} {s2:>22} " 

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

710 ) 

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

712 #@+node:ekr.20170127142001.1: *3* BaseColorizer.updateSyntaxColorer & helpers 

713 # Note: these are used by unit tests. 

714 

715 def updateSyntaxColorer(self, p): 

716 """ 

717 Scan for color directives in p and its ancestors. 

718 Return True unless an coloring is unambiguously disabled. 

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

720 """ 

721 if p: # This guard is required. 

722 try: 

723 self.enabled = self.useSyntaxColoring(p) 

724 self.language = self.scanLanguageDirectives(p) 

725 except Exception: 

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

727 g.es_exception() 

728 #@+node:ekr.20170127142001.2: *4* BaseColorizer.scanLanguageDirectives 

729 def scanLanguageDirectives(self, p): 

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

731 c = self.c 

732 language = g.getLanguageFromAncestorAtFileNode(p) 

733 return language or c.target_language 

734 #@+node:ekr.20170127142001.7: *4* BaseColorizer.useSyntaxColoring & helper 

735 def useSyntaxColoring(self, p): 

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

737 # Special cases for the selected node. 

738 d = self.findColorDirectives(p) 

739 if 'killcolor' in d: 

740 return False 

741 if 'nocolor-node' in d: 

742 return False 

743 # Now look at the parents. 

744 for p in p.parents(): 

745 d = self.findColorDirectives(p) 

746 # @killcolor anywhere disables coloring. 

747 if 'killcolor' in d: 

748 return False 

749 # unambiguous @color enables coloring. 

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

751 return True 

752 # Unambiguous @nocolor disables coloring. 

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

754 return False 

755 return True 

756 #@+node:ekr.20170127142001.8: *5* BaseColorizer.findColorDirectives 

757 # Order is important: put longest matches first. 

758 color_directives_pat = re.compile( 

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

760 , re.MULTILINE) 

761 

762 def findColorDirectives(self, p): 

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

764 d = {} 

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

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

767 d[word] = word 

768 return d 

769 #@-others 

770#@+node:ekr.20110605121601.18569: ** class JEditColorizer(BaseColorizer) 

771# This is c.frame.body.colorizer 

772 

773 

774class JEditColorizer(BaseColorizer): 

775 """ 

776 The JEditColorizer class adapts jEdit pattern matchers for QSyntaxHighlighter. 

777 For full documentation, see: 

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

779 """ 

780 #@+others 

781 #@+node:ekr.20220317050804.1: *3* jedit: Birth 

782 #@+node:ekr.20110605121601.18572: *4* jedit.__init__ & helpers 

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

784 """Ctor for JEditColorizer class.""" 

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

786 # 

787 # Create the highlighter. The default is NullObject. 

788 if isinstance(widget, QtWidgets.QTextEdit): 

789 self.highlighter = LeoHighlighter(c, 

790 colorizer=self, 

791 document=widget.document(), 

792 ) 

793 # 

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

795 self.after_doc_language = None 

796 self.initialStateNumber = -1 

797 self.old_v = None 

798 self.nextState = 1 # Dont use 0. 

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

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

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

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

803 # #2276: Set by init_section_delims. 

804 self.section_delim1 = '<<' 

805 self.section_delim2 = '>>' 

806 # 

807 # Init common data... 

808 self.reloadSettings() 

809 #@+node:ekr.20110605121601.18580: *5* jedit.init 

810 def init(self): 

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

812 # 

813 # These *must* be recomputed. 

814 self.initialStateNumber = self.setInitialStateNumber() 

815 # 

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

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

818 # self.stateDict = {} 

819 # self.stateNameDict = {} 

820 # self.restartDict = {} 

821 self.init_mode(self.language) 

822 self.clearState() 

823 # Used by matchers. 

824 self.prev = None 

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

826 self.init_section_delims() # #2276 

827 #@+node:ekr.20170201082248.1: *5* jedit.init_all_state 

828 def init_all_state(self, v): 

829 """Completely init all state data.""" 

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

831 self.old_v = v 

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

833 self.nextState = 1 # Dont use 0. 

834 self.restartDict = {} 

835 self.stateDict = {} 

836 self.stateNameDict = {} 

837 #@+node:ekr.20211029073553.1: *5* jedit.init_section_delims 

838 def init_section_delims(self): 

839 

840 p = self.c.p 

841 

842 def find_delims(v): 

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

844 m = g.g_section_delims_pat.match(s) 

845 if m: 

846 return m 

847 return None 

848 

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

850 if v: 

851 m = find_delims(v) 

852 self.section_delim1 = m.group(1) 

853 self.section_delim2 = m.group(2) 

854 else: 

855 self.section_delim1 = '<<' 

856 self.section_delim2 = '>>' 

857 #@+node:ekr.20110605121601.18576: *4* jedit.addImportedRules 

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

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

860 if self.importedRulesets.get(rulesetName): 

861 return 

862 self.importedRulesets[rulesetName] = True 

863 names = mode.importDict.get( 

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

865 for name in names: 

866 savedBunch = self.modeBunch 

867 ok = self.init_mode(name) 

868 if ok: 

869 rulesDict2 = self.rulesDict 

870 for key in rulesDict2.keys(): 

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

872 aList2 = rulesDict2.get(key) 

873 if aList2: 

874 # Don't add the standard rules again. 

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

876 if rules: 

877 aList.extend(rules) 

878 self.rulesDict[key] = aList 

879 self.initModeFromBunch(savedBunch) 

880 #@+node:ekr.20110605121601.18577: *4* jedit.addLeoRules 

881 def addLeoRules(self, theDict): 

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

883 # pylint: disable=no-member 

884 table = [ 

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

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

887 # Debatable: Leo keywords override langauge keywords. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

909 ] 

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

911 table += [ 

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

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

914 ] 

915 for ch, rule, atFront, in table: 

916 # Replace the bound method by an unbound method. 

917 rule = rule.__func__ 

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

919 if rule not in theList: 

920 if atFront: 

921 theList.insert(0, rule) 

922 else: 

923 theList.append(rule) 

924 theDict[ch] = theList 

925 #@+node:ekr.20170514054524.1: *4* jedit.getFontFromParams 

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

927 return None 

928 

929 # def setFontFromConfig(self): 

930 # pass 

931 #@+node:ekr.20110605121601.18581: *4* jedit.init_mode & helpers 

932 def init_mode(self, name): 

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

934 if not name: 

935 return False 

936 if name == 'latex': 

937 name = 'tex' 

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

939 language, rulesetName = self.nameToRulesetName(name) 

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

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

942 bunch = self.modes.get(rulesetName) 

943 if bunch: 

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

945 return False 

946 self.initModeFromBunch(bunch) 

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

948 return True 

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

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

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

952 if g.os_path_exists(fn): 

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

954 else: 

955 mode = None 

956 return self.init_mode_from_module(name, mode) 

957 #@+node:btheado.20131124162237.16303: *5* jedit.init_mode_from_module 

958 def init_mode_from_module(self, name, mode): 

959 """ 

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

961 Mode is a python module or class containing all 

962 coloring rule attributes for the mode. 

963 """ 

964 language, rulesetName = self.nameToRulesetName(name) 

965 if mode: 

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

967 if hasattr(mode, 'pre_init_mode'): 

968 mode.pre_init_mode(self.c) 

969 else: 

970 # Create a dummy bunch to limit recursion. 

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

972 attributesDict={}, 

973 defaultColor=None, 

974 keywordsDict={}, 

975 language='unknown-language', 

976 mode=mode, 

977 properties={}, 

978 rulesDict={}, 

979 rulesetName=rulesetName, 

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

981 ) 

982 self.rulesetName = rulesetName 

983 self.language = 'unknown-language' 

984 return False 

985 self.language = language 

986 self.rulesetName = rulesetName 

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

988 # 

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

990 # 

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

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

993 self.setKeywords() 

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

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

996 self.setModeAttributes() 

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

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

999 self.addLeoRules(self.rulesDict) 

1000 self.defaultColor = 'null' 

1001 self.mode = mode 

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

1003 attributesDict=self.attributesDict, 

1004 defaultColor=self.defaultColor, 

1005 keywordsDict=self.keywordsDict, 

1006 language=self.language, 

1007 mode=self.mode, 

1008 properties=self.properties, 

1009 rulesDict=self.rulesDict, 

1010 rulesetName=self.rulesetName, 

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

1012 ) 

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

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

1015 self.updateDelimsTables() 

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

1017 if initialDelegate: 

1018 # Replace the original mode by the delegate mode. 

1019 self.init_mode(initialDelegate) 

1020 language2, rulesetName2 = self.nameToRulesetName(initialDelegate) 

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

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

1023 else: 

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

1025 return True 

1026 #@+node:ekr.20110605121601.18582: *5* jedit.nameToRulesetName 

1027 def nameToRulesetName(self, name): 

1028 """ 

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

1030 name or a delegate name. 

1031 """ 

1032 if not name: 

1033 return '' 

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

1035 name = name.lower() 

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

1037 if i == -1: 

1038 language = name 

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

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

1041 rulesetName = f"{language}_main" 

1042 else: 

1043 language = name[:i] 

1044 delegate = name[i + 2 :] 

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

1046 return language, rulesetName 

1047 #@+node:ekr.20110605121601.18583: *5* jedit.setKeywords 

1048 def setKeywords(self): 

1049 """ 

1050 Initialize the keywords for the present language. 

1051 

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

1053 plus any other character appearing in any keyword. 

1054 """ 

1055 # Add any new user keywords to leoKeywordsDict. 

1056 d = self.keywordsDict 

1057 keys = list(d.keys()) 

1058 for s in g.globalDirectiveList: 

1059 key = '@' + s 

1060 if key not in keys: 

1061 d[key] = 'leokeyword' 

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

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

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

1065 for ch in key: 

1066 if ch not in chars: 

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

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

1069 # But it is needed for forth.py. 

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

1071 if ch in chars: 

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

1073 chars.remove(ch) 

1074 # Convert chars to a dict for faster access. 

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

1076 for z in chars: 

1077 self.word_chars[z] = z 

1078 #@+node:ekr.20110605121601.18584: *5* jedit.setModeAttributes 

1079 def setModeAttributes(self): 

1080 """ 

1081 Set the ivars from self.attributesDict, 

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

1083 """ 

1084 d = self.attributesDict 

1085 aList = ( 

1086 ('default', 'null'), 

1087 ('digit_re', ''), 

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

1089 ('highlight_digits', True), 

1090 ('ignore_case', True), 

1091 ('no_word_sep', ''), 

1092 ) 

1093 for key, default in aList: 

1094 val = d.get(key, default) 

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

1096 val = True 

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

1098 val = False 

1099 setattr(self, key, val) 

1100 #@+node:ekr.20110605121601.18585: *5* jedit.initModeFromBunch 

1101 def initModeFromBunch(self, bunch): 

1102 self.modeBunch = bunch 

1103 self.attributesDict = bunch.attributesDict 

1104 self.setModeAttributes() 

1105 self.defaultColor = bunch.defaultColor 

1106 self.keywordsDict = bunch.keywordsDict 

1107 self.language = bunch.language 

1108 self.mode = bunch.mode 

1109 self.properties = bunch.properties 

1110 self.rulesDict = bunch.rulesDict 

1111 self.rulesetName = bunch.rulesetName 

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

1113 #@+node:ekr.20110605121601.18586: *5* jedit.updateDelimsTables 

1114 def updateDelimsTables(self): 

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

1116 d = self.properties 

1117 lineComment = d.get('lineComment') 

1118 startComment = d.get('commentStart') 

1119 endComment = d.get('commentEnd') 

1120 if lineComment and startComment and endComment: 

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

1122 elif startComment and endComment: 

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

1124 elif lineComment: 

1125 delims = f"{lineComment}" 

1126 else: 

1127 delims = None 

1128 if delims: 

1129 d = g.app.language_delims_dict 

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

1131 d[self.language] = delims 

1132 #@+node:ekr.20110605121601.18587: *4* jedit.munge 

1133 def munge(self, s): 

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

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

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

1137 #@+node:ekr.20170205055743.1: *4* jedit.set_wikiview_patterns 

1138 def set_wikiview_patterns(self, leadins, patterns): 

1139 """ 

1140 Init the colorizer so it will *skip* all patterns. 

1141 The wikiview plugin calls this method. 

1142 """ 

1143 d = self.rulesDict 

1144 for leadins_list, pattern in zip(leadins, patterns): 

1145 for ch in leadins_list: 

1146 

1147 def wiki_rule(self, s, i, pattern=pattern): 

1148 """Bind pattern and leadin for jedit.match_wiki_pattern.""" 

1149 return self.match_wiki_pattern(s, i, pattern) 

1150 

1151 aList = d.get(ch, []) 

1152 if wiki_rule not in aList: 

1153 aList.insert(0, wiki_rule) 

1154 d[ch] = aList 

1155 self.rulesDict = d 

1156 #@+node:ekr.20110605121601.18638: *3* jedit.mainLoop 

1157 last_v = None 

1158 tot_time = 0.0 

1159 

1160 def mainLoop(self, n, s): 

1161 """Colorize a *single* line s, starting in state n.""" 

1162 f = self.restartDict.get(n) 

1163 if 'coloring' in g.app.debug: 

1164 p = self.c and self.c.p 

1165 if p and p.v != self.last_v: 

1166 self.last_v = p.v 

1167 g.trace(f"\nNEW NODE: {p.h}\n") 

1168 t1 = time.process_time() 

1169 i = f(s) if f else 0 

1170 while i < len(s): 

1171 progress = i 

1172 functions = self.rulesDict.get(s[i], []) 

1173 for f in functions: 

1174 n = f(self, s, i) 

1175 if n is None: 

1176 g.trace('Can not happen: n is None', repr(f)) 

1177 break 

1178 elif n > 0: # Success. The match has already been colored. 

1179 self.matcher_name = f.__name__ # For traces. 

1180 i += n 

1181 break 

1182 elif n < 0: # Total failure. 

1183 i += -n 

1184 break 

1185 else: # Partial failure: Do not break or change i! 

1186 pass 

1187 else: 

1188 i += 1 

1189 assert i > progress 

1190 # Don't even *think* about changing state here. 

1191 self.tot_time += time.process_time() - t1 

1192 #@+node:ekr.20110605121601.18640: *3* jedit.recolor & helpers 

1193 def recolor(self, s): 

1194 """ 

1195 jEdit.recolor: Recolor a *single* line, s. 

1196 QSyntaxHighligher calls this method repeatedly and automatically. 

1197 """ 

1198 p = self.c.p 

1199 self.recolorCount += 1 

1200 block_n = self.currentBlockNumber() 

1201 n = self.prevState() 

1202 if p.v == self.old_v: 

1203 new_language = self.n2languageDict.get(n) 

1204 if new_language != self.language: 

1205 self.language = new_language 

1206 self.init() 

1207 else: 

1208 self.updateSyntaxColorer(p) # Force a full recolor 

1209 assert self.language 

1210 self.init_all_state(p.v) 

1211 self.init() 

1212 if block_n == 0: 

1213 n = self.initBlock0() 

1214 n = self.setState(n) # Required. 

1215 # Always color the line, even if colorizing is disabled. 

1216 if s: 

1217 self.mainLoop(n, s) 

1218 #@+node:ekr.20170126100139.1: *4* jedit.initBlock0 

1219 def initBlock0(self): 

1220 """ 

1221 Init *local* ivars when handling block 0. 

1222 This prevents endless recalculation of the proper default state. 

1223 """ 

1224 if self.enabled: 

1225 n = self.setInitialStateNumber() 

1226 else: 

1227 n = self.setRestart(self.restartNoColor) 

1228 return n 

1229 #@+node:ekr.20170126101049.1: *4* jedit.setInitialStateNumber 

1230 def setInitialStateNumber(self): 

1231 """ 

1232 Init the initialStateNumber ivar for clearState() 

1233 This saves a lot of work. 

1234 

1235 Called from init() and initBlock0. 

1236 """ 

1237 state = self.languageTag(self.language) 

1238 n = self.stateNameToStateNumber(None, state) 

1239 self.initialStateNumber = n 

1240 self.blankStateNumber = self.stateNameToStateNumber(None, state + ';blank') 

1241 return n 

1242 #@+node:ekr.20170126103925.1: *4* jedit.languageTag 

1243 def languageTag(self, name): 

1244 """ 

1245 Return the standardized form of the language name. 

1246 Doing this consistently prevents subtle bugs. 

1247 """ 

1248 if name: 

1249 table = ( 

1250 ('markdown', 'md'), 

1251 ('python', 'py'), 

1252 ('javascript', 'js'), 

1253 ) 

1254 for pattern, s in table: 

1255 name = name.replace(pattern, s) 

1256 return name 

1257 return 'no-language' 

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

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

1260 #@@language rest 

1261 #@+at 

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

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

1264 # the matched text. 

1265 # 

1266 # The following arguments affect matching: 

1267 # 

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

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

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

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

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

1273 # the ruleset's escape character. 

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

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

1276 # 

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

1278 # 

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

1280 # by the indicated ruleset. 

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

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

1283 #@+node:ekr.20110605121601.18637: *4* jedit.colorRangeWithTag 

1284 def colorRangeWithTag(self, s, i, j, tag, delegate='', exclude_match=False): 

1285 """ 

1286 Actually colorize the selected range. 

1287 

1288 This is called whenever a pattern matcher succeed. 

1289 """ 

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

1291 # setTag does most tracing. 

1292 if not self.inColorState(): 

1293 # Do *not* check x.flag here. It won't work. 

1294 if trace: 

1295 g.trace('not in color state') 

1296 return 

1297 self.delegate_name = delegate 

1298 if delegate: 

1299 if trace: 

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

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

1302 else: 

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

1304 kind_s = f"{delegate}:{tag}" 

1305 print( 

1306 f"\ncolorRangeWithTag: {kind_s:25} {i:3} {j:3} " 

1307 f"{s2:>20} {self.matcher_name}\n") 

1308 self.modeStack.append(self.modeBunch) 

1309 self.init_mode(delegate) 

1310 while 0 <= i < j and i < len(s): 

1311 progress = i 

1312 assert j >= 0, j 

1313 for f in self.rulesDict.get(s[i], []): 

1314 n = f(self, s, i) 

1315 if n is None: 

1316 g.trace('Can not happen: delegate matcher returns None') 

1317 elif n > 0: 

1318 self.matcher_name = f.__name__ 

1319 i += n 

1320 break 

1321 else: 

1322 # Use the default chars for everything else. 

1323 # Use the *delegate's* default characters if possible. 

1324 default_tag = self.attributesDict.get('default') 

1325 self.setTag(default_tag or tag, s, i, i + 1) 

1326 i += 1 

1327 assert i > progress 

1328 bunch = self.modeStack.pop() 

1329 self.initModeFromBunch(bunch) 

1330 elif not exclude_match: 

1331 self.setTag(tag, s, i, j) 

1332 if tag != 'url': 

1333 # Allow UNL's and URL's *everywhere*. 

1334 j = min(j, len(s)) 

1335 while i < j: 

1336 ch = s[i].lower() 

1337 if ch == 'u': 

1338 n = self.match_unl(s, i) 

1339 i += max(1, n) 

1340 elif ch in 'fh': # file|ftp|http|https 

1341 n = self.match_any_url(s, i) 

1342 i += max(1, n) 

1343 else: 

1344 i += 1 

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

1346 def dump(self, s): 

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

1348 return s 

1349 return '\n' + s + '\n' 

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

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

1352 def match_at_color(self, s, i): 

1353 if self.trace_leo_matches: 

1354 g.trace() 

1355 # Only matches at start of line. 

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

1357 n = self.setRestart(self.restartColor) 

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

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

1360 # Now required. Sets state. 

1361 return len('@color') 

1362 return 0 

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

1364 def restartColor(self, s): 

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

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

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

1368 self.setRestart(self.restartKillColor) 

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

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

1371 self.setRestart(self.restartNoColorNode) 

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

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

1374 self.setRestart(self.restartNoColor) 

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

1376 n = self.setRestart(self.restartColor) 

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

1378 return 0 # Allow colorizing! 

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

1380 def match_at_killcolor(self, s, i): 

1381 

1382 # Only matches at start of line. 

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

1384 self.setRestart(self.restartKillColor) 

1385 return len(s) # Match everything. 

1386 return 0 

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

1388 def restartKillColor(self, s): 

1389 self.setRestart(self.restartKillColor) 

1390 return len(s) + 1 

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

1392 def match_at_language(self, s, i): 

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

1394 # Only matches at start of line. 

1395 if i != 0: 

1396 return 0 

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

1398 old_name = self.language 

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

1400 k = g.skip_c_id(s, j) 

1401 name = s[j:k] 

1402 ok = self.init_mode(name) 

1403 if ok: 

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

1405 if name != old_name: 

1406 # Solves the recoloring problem! 

1407 n = self.setInitialStateNumber() 

1408 self.setState(n) 

1409 return k - i 

1410 return 0 

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

1412 def match_at_nocolor(self, s, i): 

1413 

1414 if self.trace_leo_matches: 

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

1416 # Only matches at start of line. 

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

1418 self.setRestart(self.restartNoColor) 

1419 return len(s) # Match everything. 

1420 return 0 

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

1422 def restartNoColor(self, s): 

1423 if self.trace_leo_matches: 

1424 g.trace(repr(s)) 

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

1426 n = self.setRestart(self.restartColor) 

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

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

1429 return len('@color') 

1430 self.setRestart(self.restartNoColor) 

1431 return len(s) # Match everything. 

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

1433 def match_at_nocolor_node(self, s, i): 

1434 

1435 # Only matches at start of line. 

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

1437 self.setRestart(self.restartNoColorNode) 

1438 return len(s) # Match everything. 

1439 return 0 

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

1441 def restartNoColorNode(self, s): 

1442 self.setRestart(self.restartNoColorNode) 

1443 return len(s) + 1 

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

1445 def match_at_wrap(self, s, i): 

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

1447 c = self.c 

1448 # Only matches at start of line. 

1449 seq = '@wrap' 

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

1451 j = i + len(seq) 

1452 k = g.skip_ws(s, j) 

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

1454 c.frame.forceWrap(c.p) 

1455 return k - i 

1456 return 0 

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

1458 def match_blanks(self, s, i): 

1459 # Use Qt code to show invisibles. 

1460 return 0 

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

1462 def match_doc_part(self, s, i): 

1463 """ 

1464 Colorize Leo's @ and @ doc constructs. 

1465 Matches only at the start of the line. 

1466 """ 

1467 if i != 0: 

1468 return 0 

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

1470 j = i + 4 

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

1472 j = i + 1 

1473 else: 

1474 return 0 

1475 c = self.c 

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

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

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

1479 # Switch langauges. 

1480 self.after_doc_language = self.language 

1481 self.language = 'rest' 

1482 self.clearState() 

1483 self.init() 

1484 # Restart. 

1485 self.setRestart(self.restartDocPart) 

1486 # Do *not* color the text here! 

1487 return j 

1488 self.clearState() 

1489 self.setRestart(self.restartDocPart) 

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

1491 return len(s) 

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

1493 def restartDocPart(self, s): 

1494 """ 

1495 Restarter for @ and @ contructs. 

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

1497 """ 

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

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

1500 if tag == '@language': 

1501 return self.match_at_language(s, 0) 

1502 j = len(tag) 

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

1504 # Switch languages. 

1505 self.language = self.after_doc_language 

1506 self.clearState() 

1507 self.init() 

1508 self.after_doc_language = None 

1509 return j 

1510 # Color the next line. 

1511 self.setRestart(self.restartDocPart) 

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

1513 # Do *not* colorize the text here. 

1514 return 0 

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

1516 return len(s) 

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

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

1519 

1520 def match_image(self, s, i): 

1521 """Matcher for <img...>""" 

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

1523 if m: 

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

1525 j = len(src) 

1526 doc = self.highlighter.document() 

1527 block_n = self.currentBlockNumber() 

1528 text_block = doc.findBlockByNumber(block_n) 

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

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

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

1532 # body = self.c.frame.body 

1533 # s = body.wrapper.getAllText() 

1534 # wrapper.delete(0, j) 

1535 # cursor.insertHtml(src) 

1536 return j 

1537 return 0 

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

1539 def match_leo_keywords(self, s, i): 

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

1541 self.totalLeoKeywordsCalls += 1 

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

1543 return 0 

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

1545 i2 = i - 1 

1546 while i2 >= 0: 

1547 ch = s[i2] 

1548 if ch == '\n': 

1549 break 

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

1551 i2 -= 1 

1552 else: 

1553 return 0 

1554 # Get the word as quickly as possible. 

1555 j = i + 1 

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

1557 j += 1 

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

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

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

1561 if self.leoKeywordsDict.get(word): 

1562 kind = 'leokeyword' 

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

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

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

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

1567 return result 

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

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

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

1571 # for things that look like Leo directives. 

1572 word = '@' + word 

1573 kind = self.keywordsDict.get(word) 

1574 if kind: 

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

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

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

1578 return j - i 

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

1580 return 0 

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

1582 def match_section_ref(self, s, i): 

1583 p = self.c.p 

1584 if self.trace_leo_matches: 

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

1586 # 

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

1588 if self.language == 'patch': 

1589 return 0 

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

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

1592 return 0 

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

1594 if k == -1: 

1595 return 0 

1596 j = k + n2 

1597 # Special case for @section-delims. 

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

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

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

1601 return j - i 

1602 # An actual section reference. 

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

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

1605 if ref: 

1606 if self.use_hyperlinks: 

1607 #@+<< set the hyperlink >> 

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

1609 # Set the bindings to VNode callbacks. 

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

1611 self.hyperCount += 1 

1612 ref.tagName = tagName 

1613 #@-<< set the hyperlink >> 

1614 else: 

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

1616 else: 

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

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

1619 return j - i 

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

1621 def match_tabs(self, s, i): 

1622 # Use Qt code to show invisibles. 

1623 return 0 

1624 # Old code... 

1625 # if not self.showInvisibles: 

1626 # return 0 

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

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

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

1630 # j += 1 

1631 # if j > i: 

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

1633 # return j - i 

1634 # return 0 

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

1636 def match_trailing_ws(self, s, i): 

1637 """match trailing whitespace""" 

1638 j = i 

1639 n = len(s) 

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

1641 j += 1 

1642 if j > i and j == n: 

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

1644 return j - i 

1645 return 0 

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

1647 def match_unl(self, s, i): 

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

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

1650 # #2410: Limit the coloring if possible. 

1651 if i > 0: 

1652 ch = s[i - 1] 

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

1654 k = s.find(ch, i) 

1655 if k > -1: 

1656 j = k 

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

1658 return j 

1659 return 0 

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

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

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

1663 

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

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

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

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

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

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

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

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

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

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

1674 

1675 def match_any_url(self, s, i): 

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

1677 

1678 def match_url_f(self, s, i): 

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

1680 

1681 def match_url_g(self, s, i): 

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

1683 

1684 def match_url_h(self, s, i): 

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

1686 

1687 def match_url_m(self, s, i): 

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

1689 

1690 def match_url_n(self, s, i): 

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

1692 

1693 def match_url_p(self, s, i): 

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

1695 

1696 def match_url_t(self, s, i): 

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

1698 

1699 def match_url_w(self, s, i): 

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

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

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

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

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

1705 if n > 0: 

1706 j = i + n 

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

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

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

1710 return n 

1711 return 0 

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

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

1714 """ 

1715 Return the length of the matching text if 

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

1717 """ 

1718 # Match succeeds or fails more quickly than search. 

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

1720 if mo is None: 

1721 return 0 

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

1723 if start != i: 

1724 return 0 

1725 return end - start 

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

1727 def match_eol_span(self, s, i, 

1728 kind=None, seq='', 

1729 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1730 delegate='', exclude_match=False 

1731 ): 

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

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

1734 return 0 

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

1736 return 0 

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

1738 return 0 

1739 if at_word_start and i + len( 

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

1741 return 0 

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

1743 j = len(s) 

1744 self.colorRangeWithTag( 

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

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

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

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

1749 return 0 

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

1751 def match_eol_span_regexp(self, s, i, 

1752 kind='', regexp='', 

1753 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1754 delegate='', exclude_match=False 

1755 ): 

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

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

1758 return 0 

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

1760 return 0 

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

1762 return 0 # 7/5/2008 

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

1764 if n > 0: 

1765 j = len(s) 

1766 self.colorRangeWithTag( 

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

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

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

1770 return j - i 

1771 return 0 

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

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

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

1775 # j = len(s) 

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

1777 # return j 

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

1779 # This is a time-critical method. 

1780 

1781 def match_keywords(self, s, i): 

1782 """ 

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

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

1785 method is called. 

1786 """ 

1787 self.totalKeywordsCalls += 1 

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

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

1790 return 0 

1791 # Get the word as quickly as possible. 

1792 j = i 

1793 n = len(s) 

1794 chars = self.word_chars 

1795 # Special cases... 

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

1797 chars["'"] = "'" 

1798 if self.language == 'c': 

1799 chars['_'] = '_' 

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

1801 j += 1 

1802 word = s[i:j] 

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

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

1805 j -= 1 

1806 word = word[:-1] 

1807 if not word: 

1808 g.trace( 

1809 'can not happen', 

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

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

1812 g.callers(), 

1813 ) 

1814 return 0 

1815 if self.ignore_case: 

1816 word = word.lower() 

1817 kind = self.keywordsDict.get(word) 

1818 if kind: 

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

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

1821 result = j - i 

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

1823 return result 

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

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

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

1827 """Match the rest of the line.""" 

1828 j = g.skip_to_end_of_line(s, i) 

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

1830 return j - i 

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

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

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

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

1835 if k not in (None, 0): 

1836 return k 

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

1838 return 0 

1839 # Calculate begin and end, then just call match_span 

1840 j = i + 2 

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

1842 j += 1 

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

1844 return 0 

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

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

1847 def match_mark_following(self, s, i, 

1848 kind='', pattern='', 

1849 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1850 exclude_match=False 

1851 ): 

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

1853 if not self.allow_mark_prev: 

1854 return 0 

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

1856 return 0 

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

1858 return 0 

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

1860 return 0 # 7/5/2008 

1861 if ( 

1862 at_word_start 

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

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

1865 ): 

1866 return 0 

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

1868 j = i + len(pattern) 

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

1870 k = self.getNextToken(s, j) 

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

1872 if k > j: 

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

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

1875 j = k 

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

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

1878 return j - i 

1879 return 0 

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

1881 def getNextToken(self, s, i): 

1882 """ 

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

1884 

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

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

1887 """ 

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

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

1890 i0 = i 

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

1892 i += 1 

1893 i1 = i 

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

1895 i += 1 

1896 if i == i1: 

1897 return i0 

1898 return min(len(s), i) 

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

1900 def match_mark_previous(self, s, i, 

1901 kind='', pattern='', 

1902 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1903 exclude_match=False 

1904 ): 

1905 """ 

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

1907 

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

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

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

1911 """ 

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

1913 return 0 # 2009/6/23 

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

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

1916 """ 

1917 Return the length of the matching text if 

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

1919 """ 

1920 try: 

1921 flags = re.MULTILINE 

1922 if self.ignore_case: 

1923 flags |= re.IGNORECASE 

1924 re_obj = re.compile(pattern, flags) 

1925 except Exception: 

1926 # Do not call g.es here! 

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

1928 return 0 

1929 # Match succeeds or fails more quickly than search. 

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

1931 if mo is None: 

1932 return 0 

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

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

1935 return 0 

1936 return end - start 

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

1938 def match_seq(self, s, i, 

1939 kind='', seq='', 

1940 at_line_start=False, 

1941 at_whitespace_end=False, 

1942 at_word_start=False, 

1943 delegate='' 

1944 ): 

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

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

1947 j = i 

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

1949 j = i 

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

1951 j = i 

1952 if at_word_start and i + len( 

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

1954 j = i # 7/5/2008 

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

1956 j = i + len(seq) 

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

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

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

1960 else: 

1961 j = i 

1962 return j - i 

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

1964 def match_seq_regexp(self, s, i, 

1965 kind='', regexp='', 

1966 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1967 delegate='' 

1968 ): 

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

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

1971 return 0 

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

1973 return 0 

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

1975 return 0 

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

1977 j = i + n 

1978 assert j - i == n 

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

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

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

1982 return j - i 

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

1984 def match_span(self, s, i, 

1985 kind='', begin='', end='', 

1986 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

1987 delegate='', exclude_match=False, 

1988 no_escape=False, no_line_break=False, no_word_break=False 

1989 ): 

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

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

1992 if i >= len(s): 

1993 return 0 

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

1995 j = i 

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

1997 j = i 

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

1999 j = i 

2000 elif at_word_start and i + len( 

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

2002 j = i 

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

2004 j = i 

2005 else: 

2006 # We have matched the start of the span. 

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

2008 no_escape, no_line_break, no_word_break=no_word_break) 

2009 if j == -1: 

2010 j = i # A real failure. 

2011 else: 

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

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

2014 dots = j > len( 

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

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

2017 if dots: 

2018 kind = 'dots' + kind 

2019 # A match 

2020 i2 = i + len(begin) 

2021 j2 = j + len(end) 

2022 if delegate: 

2023 self.colorRangeWithTag( 

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

2025 self.colorRangeWithTag( 

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

2027 self.colorRangeWithTag( 

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

2029 else: 

2030 self.colorRangeWithTag( 

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

2032 j = j2 

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

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

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

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

2037 j = len(s) + 1 

2038 

2039 def span(s): 

2040 # Note: bindings are frozen by this def. 

2041 return self.restart_match_span(s, 

2042 # Positional args, in alpha order 

2043 delegate, end, exclude_match, kind, 

2044 no_escape, no_line_break, no_word_break) 

2045 

2046 self.setRestart(span, 

2047 # These must be keyword args. 

2048 delegate=delegate, end=end, 

2049 exclude_match=exclude_match, 

2050 kind=kind, 

2051 no_escape=no_escape, 

2052 no_line_break=no_line_break, 

2053 no_word_break=no_word_break) 

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

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

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

2057 """ 

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

2059 """ 

2060 esc = self.escape 

2061 # pylint: disable=inconsistent-return-statements 

2062 while 1: 

2063 j = s.find(pattern, i) 

2064 if j == -1: 

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

2066 if no_line_break: 

2067 return -1 

2068 return len(s) + 1 

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

2070 return -1 # New in Leo 4.5. 

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

2072 return -1 

2073 if esc and not no_escape: 

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

2075 escapes = 0 

2076 k = 1 

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

2078 escapes += 1 

2079 k += 1 

2080 if (escapes % 2) == 1: 

2081 assert s[j - 1] == esc 

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

2083 else: 

2084 return j 

2085 else: 

2086 return j 

2087 # For pylint. 

2088 return -1 

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

2090 def restart_match_span(self, s, 

2091 delegate, end, exclude_match, kind, 

2092 no_escape, no_line_break, no_word_break 

2093 ): 

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

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

2096 i = 0 

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

2098 if j == -1: 

2099 j2 = len(s) + 1 

2100 elif j > len(s): 

2101 j2 = j 

2102 else: 

2103 j2 = j + len(end) 

2104 if delegate: 

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

2106 delegate=delegate, exclude_match=exclude_match) 

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

2108 delegate=None, exclude_match=exclude_match) 

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

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

2111 delegate=None, exclude_match=exclude_match) 

2112 j = j2 

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

2114 if j > len(s): 

2115 

2116 def span(s): 

2117 return self.restart_match_span(s, 

2118 # Positional args, in alpha order 

2119 delegate, end, exclude_match, kind, 

2120 no_escape, no_line_break, no_word_break) 

2121 

2122 self.setRestart(span, 

2123 # These must be keywords args. 

2124 delegate=delegate, end=end, kind=kind, 

2125 no_escape=no_escape, 

2126 no_line_break=no_line_break, 

2127 no_word_break=no_word_break) 

2128 else: 

2129 self.clearState() 

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

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

2132 def match_span_regexp(self, s, i, 

2133 kind='', begin='', end='', 

2134 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

2135 delegate='', exclude_match=False, 

2136 no_escape=False, no_line_break=False, no_word_break=False, 

2137 ): 

2138 """ 

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

2140 contains a following 'end'. 

2141 """ 

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

2143 return 0 

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

2145 return 0 

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

2147 return 0 # 7/5/2008 

2148 if ( 

2149 at_word_start 

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

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

2152 ): 

2153 return 0 # 7/5/2008 

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

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

2156 if n > 0: 

2157 j = i + n 

2158 j2 = s.find(end, j) 

2159 if j2 == -1: 

2160 return 0 

2161 if self.escape and not no_escape: 

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

2163 escapes = 0 

2164 k = 1 

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

2166 escapes += 1 

2167 k += 1 

2168 if (escapes % 2) == 1: 

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

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

2171 return 0 

2172 i2 = j2 - len(end) 

2173 if delegate: 

2174 self.colorRangeWithTag( 

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

2176 self.colorRangeWithTag( 

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

2178 self.colorRangeWithTag( 

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

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

2181 self.colorRangeWithTag( 

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

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

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

2185 return j2 - i 

2186 return 0 

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

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

2189 

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

2191 """ 

2192 Match the tex s[i:]. 

2193 

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

2195 1. One or more ascii letters, or 

2196 2. Exactly one character, of any kind. 

2197 """ 

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

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

2200 if m: 

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

2202 j = i + n + 1 

2203 else: 

2204 # Colorize the backslash plus exactly one more character. 

2205 j = i + 2 

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

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

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

2209 return j - i 

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

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

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

2213 m = pattern.match(s, i) 

2214 if m: 

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

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

2217 return n 

2218 return 0 

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

2220 def match_word_and_regexp(self, s, i, 

2221 kind1='', word='', 

2222 kind2='', pattern='', 

2223 at_line_start=False, at_whitespace_end=False, at_word_start=False, 

2224 exclude_match=False 

2225 ): 

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

2227 if not self.allow_mark_prev: 

2228 return 0 

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

2230 return 0 

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

2232 return 0 

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

2234 return 0 

2235 if ( 

2236 at_word_start 

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

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

2239 ): 

2240 j = i 

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

2242 return 0 

2243 j = i + len(word) 

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

2245 if n == 0: 

2246 return 0 

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

2248 k = j + n 

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

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

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

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

2253 return k - i 

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

2255 def skip_line(self, s, i): 

2256 if self.escape: 

2257 escape = self.escape + '\n' 

2258 n = len(escape) 

2259 while i < len(s): 

2260 j = g.skip_line(s, i) 

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

2262 return j 

2263 i = j 

2264 return i 

2265 return g.skip_line(s, i) 

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

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

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

2269 

2270 if j != i and self.trace_match_flag: 

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

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

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

2274 def clearState(self): 

2275 """ 

2276 Create a *language-specific* default state. 

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

2278 """ 

2279 n = self.initialStateNumber 

2280 self.setState(n) 

2281 return n 

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

2283 def computeState(self, f, keys): 

2284 """ 

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

2286 Return a unique int n representing that state. 

2287 """ 

2288 # Abbreviate arg names. 

2289 d = { 

2290 'delegate': '=>', 

2291 'end': 'end', 

2292 'at_line_start': 'start', 

2293 'at_whitespace_end': 'ws-end', 

2294 'exclude_match': '!match', 

2295 'no_escape': '!esc', 

2296 'no_line_break': '!lbrk', 

2297 'no_word_break': '!wbrk', 

2298 } 

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

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

2301 result.append(self.rulesetName) 

2302 if f: 

2303 result.append(f.__name__) 

2304 for key in sorted(keys): 

2305 keyVal = keys.get(key) 

2306 val = d.get(key) 

2307 if val is None: 

2308 val = keys.get(key) 

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

2310 elif keyVal is True: 

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

2312 elif keyVal is False: 

2313 pass 

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

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

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

2317 table = ( 

2318 ('kind=', ''), 

2319 ('literal', 'lit'), 

2320 ('restart', '@'), 

2321 ) 

2322 for pattern, s in table: 

2323 state = state.replace(pattern, s) 

2324 n = self.stateNameToStateNumber(f, state) 

2325 return n 

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

2327 def currentBlockNumber(self): 

2328 block = self.highlighter.currentBlock() 

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

2330 

2331 def currentState(self): 

2332 return self.highlighter.currentBlockState() 

2333 

2334 def prevState(self): 

2335 return self.highlighter.previousBlockState() 

2336 

2337 def setState(self, n): 

2338 self.highlighter.setCurrentBlockState(n) 

2339 return n 

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

2341 def inColorState(self): 

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

2343 n = self.currentState() 

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

2345 enabled = ( 

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

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

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

2349 return enabled 

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

2351 def setRestart(self, f, **keys): 

2352 n = self.computeState(f, keys) 

2353 self.setState(n) 

2354 return n 

2355 #@+node:ekr.20110605121601.18635: *4* jedit.show... 

2356 def showState(self, n): 

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

2358 return f"{n:2}:{state}" 

2359 

2360 def showCurrentState(self): 

2361 n = self.currentState() 

2362 return self.showState(n) 

2363 

2364 def showPrevState(self): 

2365 n = self.prevState() 

2366 return self.showState(n) 

2367 #@+node:ekr.20110605121601.18636: *4* jedit.stateNameToStateNumber 

2368 def stateNameToStateNumber(self, f, stateName): 

2369 """ 

2370 stateDict: Keys are state numbers, values state names. 

2371 stateNameDict: Keys are state names, values are state numbers. 

2372 restartDict: Keys are state numbers, values are restart functions 

2373 """ 

2374 n = self.stateNameDict.get(stateName) 

2375 if n is None: 

2376 n = self.nextState 

2377 self.stateNameDict[stateName] = n 

2378 self.stateDict[n] = stateName 

2379 self.restartDict[n] = f 

2380 self.nextState += 1 

2381 self.n2languageDict[n] = self.language 

2382 return n 

2383 #@-others 

2384#@+node:ekr.20110605121601.18565: ** class LeoHighlighter (QSyntaxHighlighter) 

2385# Careful: we may be running from the bridge. 

2386 

2387if QtGui: 

2388 

2389 

2390 class LeoHighlighter(QtGui.QSyntaxHighlighter): # type:ignore 

2391 """ 

2392 A subclass of QSyntaxHighlighter that overrides 

2393 the highlightBlock and rehighlight methods. 

2394 

2395 All actual syntax coloring is done in the highlighter class. 

2396 

2397 Used by both the JeditColorizer and PYgmentsColorizer classes. 

2398 """ 

2399 # This is c.frame.body.colorizer.highlighter 

2400 #@+others 

2401 #@+node:ekr.20110605121601.18566: *3* leo_h.ctor (sets style) 

2402 def __init__(self, c, colorizer, document): 

2403 """ctor for LeoHighlighter class.""" 

2404 self.c = c 

2405 self.colorizer = colorizer 

2406 self.n_calls = 0 

2407 assert isinstance(document, QtGui.QTextDocument), document 

2408 # Alas, a QsciDocument is not a QTextDocument. 

2409 self.leo_document = document 

2410 super().__init__(document) 

2411 self.reloadSettings() 

2412 #@+node:ekr.20110605121601.18567: *3* leo_h.highlightBlock 

2413 def highlightBlock(self, s): 

2414 """ Called by QSyntaxHighlighter """ 

2415 self.n_calls += 1 

2416 s = g.toUnicode(s) 

2417 self.colorizer.recolor(s) 

2418 # Highlight just one line. 

2419 #@+node:ekr.20190327052228.1: *3* leo_h.reloadSettings 

2420 def reloadSettings(self): 

2421 """Reload all reloadable settings.""" 

2422 c, document = self.c, self.leo_document 

2423 if not pygments: 

2424 return 

2425 if not c.config.getBool('use-pygments', default=False): 

2426 return 

2427 # Init pygments ivars. 

2428 self._brushes = {} 

2429 self._document = document 

2430 self._formats = {} 

2431 self.colorizer.style_name = 'default' 

2432 # Style gallery: https://help.farbox.com/pygments.html 

2433 # Dark styles: fruity, monokai, native, vim 

2434 # https://github.com/gthank/solarized-dark-pygments 

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

2436 if not c.config.getBool('use-pygments-styles', default=True): 

2437 return 

2438 # Init pygments style. 

2439 try: 

2440 self.setStyle(style_name) 

2441 # print('using %r pygments style in %r' % (style_name, c.shortFileName())) 

2442 except Exception: 

2443 print(f'pygments {style_name!r} style not found. Using "default" style') 

2444 self.setStyle('default') 

2445 style_name = 'default' 

2446 self.colorizer.style_name = style_name 

2447 assert self._style 

2448 #@+node:ekr.20190320154014.1: *3* leo_h: From PygmentsHighlighter 

2449 # 

2450 # All code in this tree is based on PygmentsHighlighter. 

2451 # 

2452 # Copyright (c) Jupyter Development Team. 

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

2454 #@+others 

2455 #@+node:ekr.20190320153605.1: *4* leo_h._get_format & helpers 

2456 def _get_format(self, token): 

2457 """ Returns a QTextCharFormat for token or None. 

2458 """ 

2459 if token in self._formats: 

2460 return self._formats[token] 

2461 if self._style is None: 

2462 result = self._get_format_from_document(token, self._document) 

2463 else: 

2464 result = self._get_format_from_style(token, self._style) 

2465 result = self._get_format_from_style(token, self._style) 

2466 self._formats[token] = result 

2467 return result 

2468 #@+node:ekr.20190320162831.1: *5* pyg_h._get_format_from_document 

2469 def _get_format_from_document(self, token, document): 

2470 """ Returns a QTextCharFormat for token by 

2471 """ 

2472 # Modified by EKR. 

2473 # These lines cause unbounded recursion. 

2474 # code, html = next(self._formatter._format_lines([(token, u'dummy')])) 

2475 # self._document.setHtml(html) 

2476 return QtGui.QTextCursor(self._document).charFormat() 

2477 #@+node:ekr.20190320153716.1: *5* leo_h._get_format_from_style 

2478 key_error_d: Dict[str, bool] = {} 

2479 

2480 def _get_format_from_style(self, token, style): 

2481 """ Returns a QTextCharFormat for token by reading a Pygments style. 

2482 """ 

2483 result = QtGui.QTextCharFormat() 

2484 # 

2485 # EKR: handle missing tokens. 

2486 try: 

2487 data = style.style_for_token(token).items() 

2488 except KeyError as err: 

2489 key = repr(err) 

2490 if key not in self.key_error_d: 

2491 self.key_error_d[key] = True 

2492 g.trace(err) 

2493 return result 

2494 for key, value in data: 

2495 if value: 

2496 if key == 'color': 

2497 result.setForeground(self._get_brush(value)) 

2498 elif key == 'bgcolor': 

2499 result.setBackground(self._get_brush(value)) 

2500 elif key == 'bold': 

2501 result.setFontWeight(Weight.Bold) 

2502 elif key == 'italic': 

2503 result.setFontItalic(True) 

2504 elif key == 'underline': 

2505 result.setUnderlineStyle(UnderlineStyle.SingleUnderline) 

2506 elif key == 'sans': 

2507 result.setFontStyleHint(Weight.SansSerif) 

2508 elif key == 'roman': 

2509 result.setFontStyleHint(Weight.Times) 

2510 elif key == 'mono': 

2511 result.setFontStyleHint(Weight.TypeWriter) 

2512 return result 

2513 #@+node:ekr.20190320153958.1: *4* leo_h.setStyle 

2514 def setStyle(self, style): 

2515 """ Sets the style to the specified Pygments style. 

2516 """ 

2517 from pygments.styles import get_style_by_name # type:ignore 

2518 

2519 if isinstance(style, str): 

2520 style = get_style_by_name(style) 

2521 self._style = style 

2522 self._clear_caches() 

2523 #@+node:ekr.20190320154604.1: *4* leo_h.clear_caches 

2524 def _clear_caches(self): 

2525 """ Clear caches for brushes and formats. 

2526 """ 

2527 self._brushes = {} 

2528 self._formats = {} 

2529 #@+node:ekr.20190320154752.1: *4* leo_h._get_brush/color 

2530 def _get_brush(self, color): 

2531 """ Returns a brush for the color. 

2532 """ 

2533 result = self._brushes.get(color) 

2534 if result is None: 

2535 qcolor = self._get_color(color) 

2536 result = QtGui.QBrush(qcolor) 

2537 self._brushes[color] = result 

2538 return result 

2539 

2540 def _get_color(self, color): 

2541 """ Returns a QColor built from a Pygments color string. 

2542 """ 

2543 qcolor = QtGui.QColor() 

2544 qcolor.setRgb(int(color[:2], base=16), 

2545 int(color[2:4], base=16), 

2546 int(color[4:6], base=16)) 

2547 return qcolor 

2548 #@-others 

2549 #@-others 

2550#@+node:ekr.20140906095826.18717: ** class NullScintillaLexer (QsciLexerCustom) 

2551if Qsci: 

2552 

2553 

2554 class NullScintillaLexer(Qsci.QsciLexerCustom): # type:ignore 

2555 """A do-nothing colorizer for Scintilla.""" 

2556 

2557 def __init__(self, c, parent=None): 

2558 super().__init__(parent) 

2559 # Init the pase class 

2560 self.leo_c = c 

2561 self.configure_lexer() 

2562 

2563 def description(self, style): 

2564 return 'NullScintillaLexer' 

2565 

2566 def setStyling(self, length, style): 

2567 g.trace('(NullScintillaLexer)', length, style) 

2568 

2569 def styleText(self, start, end): 

2570 """Style the text from start to end.""" 

2571 

2572 def configure_lexer(self): 

2573 """Configure the QScintilla lexer.""" 

2574 # c = self.leo_c 

2575 lexer = self 

2576 # To do: use c.config setting. 

2577 # pylint: disable=no-member 

2578 font = QtGui.QFont("DejaVu Sans Mono", 14) 

2579 lexer.setFont(font) 

2580#@+node:ekr.20190319151826.1: ** class PygmentsColorizer(BaseColorizer) 

2581class PygmentsColorizer(BaseColorizer): 

2582 """ 

2583 This class adapts pygments tokens to QSyntaxHighlighter. 

2584 """ 

2585 # This is c.frame.body.colorizer 

2586 #@+others 

2587 #@+node:ekr.20220317053040.1: *3* pyg_c: Birth 

2588 #@+node:ekr.20190319151826.3: *4* pyg_c.__init__ 

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

2590 """Ctor for PygmentsColorizer class.""" 

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

2592 # Create the highlighter. The default is NullObject. 

2593 if isinstance(widget, QtWidgets.QTextEdit): 

2594 self.highlighter = LeoHighlighter(c, 

2595 colorizer=self, 

2596 document=widget.document(), 

2597 ) 

2598 # State unique to this class... 

2599 self.color_enabled = self.enabled 

2600 self.old_v = None 

2601 # Monkey-patch g.isValidLanguage. 

2602 g.isValidLanguage = self.pygments_isValidLanguage 

2603 # Init common data... 

2604 self.reloadSettings() 

2605 #@+node:ekr.20190324063349.1: *4* pyg_c.format getters 

2606 def getLegacyDefaultFormat(self): 

2607 return None 

2608 

2609 def getLegacyFormat(self, token, text): 

2610 """Return a jEdit tag for the given pygments token.""" 

2611 # Tables and setTag assume lower-case. 

2612 r = repr(token).lstrip('Token.').lstrip('Literal.').lower() 

2613 if r == 'name': 

2614 # Avoid a colision with existing Leo tag. 

2615 r = 'name.pygments' 

2616 return r 

2617 

2618 def getPygmentsFormat(self, token, text): 

2619 """Return a pygments format.""" 

2620 format = self.highlighter._formats.get(token) 

2621 if not format: 

2622 format = self.highlighter._get_format(token) 

2623 return format 

2624 #@+node:ekr.20190324064341.1: *4* pyg_c.format setters 

2625 def setLegacyFormat(self, index, length, format, s): 

2626 """Call the jEdit style setTag.""" 

2627 super().setTag(format, s, index, index + length) 

2628 

2629 def setPygmentsFormat(self, index, length, format, s): 

2630 """Call the base setTag to set the Qt format.""" 

2631 self.highlighter.setFormat(index, length, format) 

2632 #@+node:ekr.20220316200022.1: *3* pyg_c.pygments_isValidLanguage 

2633 def pygments_isValidLanguage(self, language: str) -> bool: 

2634 """ 

2635 A hack: we will monkey-patch g.isValidLanguage to be this method. 

2636  

2637 Without this hack this class would have to define its own copy of the 

2638 (complex!) g.getLanguageFromAncestorAtFileNode function. 

2639 """ 

2640 lexer_name = 'python3' if language == 'python' else language 

2641 try: 

2642 import pygments.lexers as lexers # type: ignore 

2643 lexers.get_lexer_by_name(lexer_name) 

2644 return True 

2645 except Exception: 

2646 return False 

2647 #@+node:ekr.20190324051704.1: *3* pyg_c.reloadSettings 

2648 def reloadSettings(self): 

2649 """Reload the base settings, plus pygments settings.""" 

2650 # Do basic inits. 

2651 super().reloadSettings() 

2652 # Bind methods. 

2653 if self.use_pygments_styles: 

2654 self.getDefaultFormat = QtGui.QTextCharFormat 

2655 self.getFormat = self.getPygmentsFormat 

2656 self.setFormat = self.setPygmentsFormat 

2657 else: 

2658 self.getDefaultFormat = self.getLegacyDefaultFormat 

2659 self.getFormat = self.getLegacyFormat 

2660 self.setFormat = self.setLegacyFormat 

2661 #@+node:ekr.20190319151826.78: *3* pyg_c.mainLoop & helpers 

2662 format_dict: Dict[str, str] = {} # Keys are repr(Token), values are formats. 

2663 lexers_dict: Dict[str, Callable] = {} # Keys are language names, values are instantiated, patched lexers. 

2664 state_s_dict: Dict[str, int] = {} # Keys are strings, values are ints. 

2665 state_n_dict: Dict[int, str] = {} # # Keys are ints, values are strings. 

2666 state_index = 1 # Index of state number to be allocated. 

2667 # For traces. 

2668 last_v = None 

2669 tot_time = 0.0 

2670 

2671 def mainLoop(self, s): 

2672 """Colorize a *single* line s""" 

2673 if 'coloring' in g.app.debug: 

2674 p = self.c and self.c.p 

2675 if p and p.v != self.last_v: 

2676 self.last_v = p.v 

2677 g.trace(f"\nNEW NODE: {p.h}\n") 

2678 t1 = time.process_time() 

2679 highlighter = self.highlighter 

2680 # 

2681 # First, set the *expected* lexer. It may change later. 

2682 lexer = self.set_lexer() 

2683 # 

2684 # Restore the state. 

2685 # Based on Jupyter code: (c) Jupyter Development Team. 

2686 stack_ivar = '_saved_state_stack' 

2687 prev_data = highlighter.currentBlock().previous().userData() 

2688 if prev_data is not None: 

2689 # New code by EKR. Restore the language if necessary. 

2690 if self.language != prev_data.leo_language: 

2691 # Change the language and the lexer! 

2692 self.language = prev_data.leo_language 

2693 lexer = self.set_lexer() 

2694 setattr(lexer, stack_ivar, prev_data.syntax_stack) 

2695 elif hasattr(lexer, stack_ivar): 

2696 delattr(lexer, stack_ivar) 

2697 # 

2698 # The main loop. Warning: this can change self.language. 

2699 index = 0 

2700 for token, text in lexer.get_tokens(s): 

2701 length = len(text) 

2702 if self.color_enabled: 

2703 format = self.getFormat(token, text) 

2704 else: 

2705 format = self.getDefaultFormat() 

2706 self.setFormat(index, length, format, s) 

2707 index += length 

2708 # 

2709 # Save the state. 

2710 # Based on Jupyter code: (c) Jupyter Development Team. 

2711 stack = getattr(lexer, stack_ivar, None) 

2712 if stack: 

2713 data = PygmentsBlockUserData(syntax_stack=stack, leo_language=self.language) 

2714 highlighter.currentBlock().setUserData(data) 

2715 # Clean up for the next go-round. 

2716 delattr(lexer, stack_ivar) 

2717 # 

2718 # New code by EKR: 

2719 # - Fixes a bug so multiline tokens work. 

2720 # - State supports Leo's color directives. 

2721 state_s = f"{self.language}; {self.color_enabled}: {stack!r}" 

2722 state_n = self.state_s_dict.get(state_s) 

2723 if state_n is None: 

2724 state_n = self.state_index 

2725 self.state_index += 1 

2726 self.state_s_dict[state_s] = state_n 

2727 self.state_n_dict[state_n] = state_s 

2728 highlighter.setCurrentBlockState(state_n) 

2729 self.tot_time += time.process_time() - t1 

2730 #@+node:ekr.20190323045655.1: *4* pyg_c.at_color_callback 

2731 def at_color_callback(self, lexer, match): 

2732 from pygments.token import Name, Text # type: ignore 

2733 kind = match.group(0) 

2734 self.color_enabled = kind == '@color' 

2735 if self.color_enabled: 

2736 yield match.start(), Name.Decorator, kind 

2737 else: 

2738 yield match.start(), Text, kind 

2739 #@+node:ekr.20190323045735.1: *4* pyg_c.at_language_callback 

2740 def at_language_callback(self, lexer, match): 

2741 """Colorize the name only if the language has a lexer.""" 

2742 from pygments.token import Name 

2743 language = match.group(2) 

2744 # #2484: The language is known if there is a lexer for it. 

2745 if self.pygments_isValidLanguage(language): 

2746 self.language = language 

2747 yield match.start(), Name.Decorator, match.group(0) 

2748 else: 

2749 # Color only the @language, indicating an unknown language. 

2750 yield match.start(), Name.Decorator, match.group(1) 

2751 #@+node:ekr.20190322082533.1: *4* pyg_c.get_lexer 

2752 unknown_languages: List[str] = [] 

2753 

2754 def get_lexer(self, language): 

2755 """Return the lexer for self.language, creating it if necessary.""" 

2756 import pygments.lexers as lexers # type: ignore 

2757 trace = 'coloring' in g.app.debug 

2758 try: 

2759 # #1520: always define lexer_language. 

2760 lexer_name = 'python3' if language == 'python' else language 

2761 lexer = lexers.get_lexer_by_name(lexer_name) 

2762 except Exception: 

2763 # One of the lexer's will not exist. 

2764 # pylint: disable=no-member 

2765 if trace and language not in self.unknown_languages: 

2766 self.unknown_languages.append(language) 

2767 g.trace(f"\nno lexer for {language!r}. Using python 3 lexer\n") 

2768 lexer = lexers.Python3Lexer() 

2769 return lexer 

2770 #@+node:ekr.20190322094034.1: *4* pyg_c.patch_lexer 

2771 def patch_lexer(self, language, lexer): 

2772 

2773 from pygments.token import Comment # type:ignore 

2774 from pygments.lexer import inherit # type:ignore 

2775 

2776 

2777 class PatchedLexer(lexer.__class__): # type:ignore 

2778 

2779 leo_sec_ref_pat = r'(?-m:\<\<(.*?)\>\>)' 

2780 tokens = { 

2781 'root': [ 

2782 (r'^@(color|nocolor|killcolor)\b', self.at_color_callback), 

2783 (r'^(@language)\s+(\w+)', self.at_language_callback), 

2784 (leo_sec_ref_pat, self.section_ref_callback), 

2785 # Single-line, non-greedy match. 

2786 (r'(^\s*@doc|@)(\s+|\n)(.|\n)*?^@c', Comment.Leo.DocPart), 

2787 # Multi-line, non-greedy match. 

2788 inherit, 

2789 ], 

2790 } 

2791 

2792 try: 

2793 return PatchedLexer() 

2794 except Exception: 

2795 g.trace(f"can not patch {language!r}") 

2796 g.es_exception() 

2797 return lexer 

2798 #@+node:ekr.20190322133358.1: *4* pyg_c.section_ref_callback 

2799 def section_ref_callback(self, lexer, match): 

2800 """pygments callback for section references.""" 

2801 c = self.c 

2802 from pygments.token import Comment, Name 

2803 name, ref, start = match.group(1), match.group(0), match.start() 

2804 found = g.findReference(ref, c.p) 

2805 found_tok = Name.Entity if found else Name.Other 

2806 yield match.start(), Comment, '<<' 

2807 yield start + 2, found_tok, name 

2808 yield start + 2 + len(name), Comment, '>>' 

2809 #@+node:ekr.20190323064820.1: *4* pyg_c.set_lexer 

2810 def set_lexer(self): 

2811 """Return the lexer for self.language.""" 

2812 if self.language == 'patch': 

2813 self.language = 'diff' 

2814 key = f"{self.language}:{id(self)}" 

2815 lexer = self.lexers_dict.get(key) 

2816 if not lexer: 

2817 lexer = self.get_lexer(self.language) 

2818 lexer = self.patch_lexer(self.language, lexer) 

2819 self.lexers_dict[key] = lexer 

2820 return lexer 

2821 #@+node:ekr.20190319151826.79: *3* pyg_c.recolor 

2822 def recolor(self, s): 

2823 """ 

2824 PygmentsColorizer.recolor: Recolor a *single* line, s. 

2825 QSyntaxHighligher calls this method repeatedly and automatically. 

2826 """ 

2827 p = self.c.p 

2828 self.recolorCount += 1 

2829 if p.v != self.old_v: 

2830 self.updateSyntaxColorer(p) 

2831 # Force a full recolor 

2832 # sets self.language and self.enabled. 

2833 self.color_enabled = self.enabled 

2834 self.old_v = p.v # Fix a major performance bug. 

2835 self.init() 

2836 assert self.language 

2837 if s is not None: 

2838 # For pygments, we *must* call for all lines. 

2839 self.mainLoop(s) 

2840 #@-others 

2841#@+node:ekr.20140906081909.18689: ** class QScintillaColorizer(BaseColorizer) 

2842# This is c.frame.body.colorizer 

2843 

2844 

2845class QScintillaColorizer(BaseColorizer): 

2846 """A colorizer for a QsciScintilla widget.""" 

2847 #@+others 

2848 #@+node:ekr.20140906081909.18709: *3* qsc.__init__ & reloadSettings 

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

2850 """Ctor for QScintillaColorizer. widget is a """ 

2851 super().__init__(c) 

2852 self.count = 0 # For unit testing. 

2853 self.colorCacheFlag = False 

2854 self.error = False # Set if there is an error in jeditColorizer.recolor 

2855 self.flag = True # Per-node enable/disable flag. 

2856 self.full_recolor_count = 0 # For unit testing. 

2857 self.language = 'python' # set by scanLanguageDirectives. 

2858 self.highlighter = None 

2859 self.lexer = None # Set in changeLexer. 

2860 widget.leo_colorizer = self 

2861 # Define/configure various lexers. 

2862 self.reloadSettings() 

2863 if Qsci: 

2864 self.lexersDict = self.makeLexersDict() 

2865 self.nullLexer = NullScintillaLexer(c) 

2866 else: 

2867 self.lexersDict = {} # type:ignore 

2868 self.nullLexer = g.NullObject() # type:ignore 

2869 

2870 def reloadSettings(self): 

2871 c = self.c 

2872 self.enabled = c.config.getBool('use-syntax-coloring') 

2873 #@+node:ekr.20170128141158.1: *3* qsc.scanColorDirectives (over-ride) 

2874 def scanColorDirectives(self, p): 

2875 """ 

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

2877 Same as BaseColorizer.scanColorDirectives, except it also scans p.b. 

2878 """ 

2879 c = self.c 

2880 root = p.copy() 

2881 for p in root.self_and_parents(copy=False): 

2882 language = g.findFirstValidAtLanguageDirective(p.b) 

2883 if language: 

2884 return language 

2885 # Get the language from the nearest ancestor @<file> node. 

2886 language = g.getLanguageFromAncestorAtFileNode(root) or c.target_language 

2887 return language 

2888 #@+node:ekr.20140906081909.18718: *3* qsc.changeLexer 

2889 def changeLexer(self, language): 

2890 """Set the lexer for the given language.""" 

2891 c = self.c 

2892 wrapper = c.frame.body.wrapper 

2893 w = wrapper.widget # A Qsci.QsciSintilla object. 

2894 self.lexer = self.lexersDict.get(language, self.nullLexer) # type:ignore 

2895 w.setLexer(self.lexer) 

2896 #@+node:ekr.20140906081909.18707: *3* qsc.colorize 

2897 def colorize(self, p): 

2898 """The main Scintilla colorizer entry point.""" 

2899 # It would be much better to use QSyntaxHighlighter. 

2900 # Alas, a QSciDocument is not a QTextDocument. 

2901 self.updateSyntaxColorer(p) 

2902 self.changeLexer(self.language) 

2903 # if self.NEW: 

2904 # # Works, but QScintillaWrapper.tag_configuration is presently a do-nothing. 

2905 # for s in g.splitLines(p.b): 

2906 # self.jeditColorizer.recolor(s) 

2907 #@+node:ekr.20140906095826.18721: *3* qsc.configure_lexer 

2908 def configure_lexer(self, lexer): 

2909 """Configure the QScintilla lexer using @data qt-scintilla-styles.""" 

2910 c = self.c 

2911 qcolor, qfont = QtGui.QColor, QtGui.QFont 

2912 font = qfont("DejaVu Sans Mono", 14) 

2913 lexer.setFont(font) 

2914 lexer.setEolFill(False, -1) 

2915 if hasattr(lexer, 'setStringsOverNewlineAllowed'): 

2916 lexer.setStringsOverNewlineAllowed(False) 

2917 table: List[Tuple[str, str]] = [] 

2918 aList = c.config.getData('qt-scintilla-styles') 

2919 if aList: 

2920 aList = [s.split(',') for s in aList] 

2921 for z in aList: 

2922 if len(z) == 2: 

2923 color, style = z 

2924 table.append((color.strip(), style.strip()),) 

2925 else: g.trace(f"entry: {z}") 

2926 if not table: 

2927 black = '#000000' 

2928 firebrick3 = '#CD2626' 

2929 leo_green = '#00aa00' 

2930 # See http://pyqt.sourceforge.net/Docs/QScintilla2/classQsciLexerPython.html 

2931 # for list of selector names. 

2932 table = [ 

2933 # EKR's personal settings are reasonable defaults. 

2934 (black, 'ClassName'), 

2935 (firebrick3, 'Comment'), 

2936 (leo_green, 'Decorator'), 

2937 (leo_green, 'DoubleQuotedString'), 

2938 (black, 'FunctionMethodName'), 

2939 ('blue', 'Keyword'), 

2940 (black, 'Number'), 

2941 (leo_green, 'SingleQuotedString'), 

2942 (leo_green, 'TripleSingleQuotedString'), 

2943 (leo_green, 'TripleDoubleQuotedString'), 

2944 (leo_green, 'UnclosedString'), 

2945 # End of line where string is not closed 

2946 # style.python.13=fore:#000000,$(font.monospace),back:#E0C0E0,eolfilled 

2947 ] 

2948 for color, style in table: 

2949 if hasattr(lexer, style): 

2950 style_number = getattr(lexer, style) 

2951 try: 

2952 lexer.setColor(qcolor(color), style_number) 

2953 except Exception: 

2954 g.trace('bad color', color) 

2955 else: 

2956 pass 

2957 # Not an error. Not all lexers have all styles. 

2958 # g.trace('bad style: %s.%s' % (lexer.__class__.__name__, style)) 

2959 #@+node:ekr.20170128031840.1: *3* qsc.init 

2960 def init(self): 

2961 """QScintillaColorizer.init""" 

2962 self.updateSyntaxColorer(self.c.p) 

2963 self.changeLexer(self.language) 

2964 #@+node:ekr.20170128133525.1: *3* qsc.makeLexersDict 

2965 def makeLexersDict(self): 

2966 """Make a dictionary of Scintilla lexers, and configure each one.""" 

2967 c = self.c 

2968 # g.printList(sorted(dir(Qsci))) 

2969 parent = c.frame.body.wrapper.widget 

2970 table = ( 

2971 # 'Asm', 'Erlang', 'Forth', 'Haskell', 

2972 # 'LaTeX', 'Lisp', 'Markdown', 'Nsis', 'R', 

2973 'Bash', 'Batch', 'CPP', 'CSS', 'CMake', 'CSharp', 'CoffeeScript', 

2974 'D', 'Diff', 'Fortran', 'Fortran77', 'HTML', 

2975 'Java', 'JavaScript', 'Lua', 'Makefile', 'Matlab', 

2976 'Pascal', 'Perl', 'Python', 'PostScript', 'Properties', 

2977 'Ruby', 'SQL', 'TCL', 'TeX', 'XML', 'YAML', 

2978 ) 

2979 d = {} 

2980 for language_name in table: 

2981 class_name = 'QsciLexer' + language_name 

2982 lexer_class = getattr(Qsci, class_name, None) 

2983 if lexer_class: 

2984 # pylint: disable=not-callable 

2985 lexer = lexer_class(parent=parent) 

2986 self.configure_lexer(lexer) 

2987 d[language_name.lower()] = lexer 

2988 elif 0: 

2989 g.trace('no lexer for', class_name) 

2990 return d 

2991 #@-others 

2992#@+node:ekr.20190320062618.1: ** Jupyter classes 

2993# Copyright (c) Jupyter Development Team. 

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

2995 

2996if pygments: 

2997 #@+others 

2998 #@+node:ekr.20190320062624.2: *3* RegexLexer.get_tokens_unprocessed 

2999 # Copyright (c) Jupyter Development Team. 

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

3001 

3002 from pygments.lexer import RegexLexer, _TokenType, Text, Error 

3003 

3004 def get_tokens_unprocessed(self, text, stack=('root',)): 

3005 """ 

3006 Split ``text`` into (tokentype, text) pairs. 

3007 

3008 Monkeypatched to store the final stack on the object itself. 

3009 

3010 The `text` parameter this gets passed is only the current line, so to 

3011 highlight things like multiline strings correctly, we need to retrieve 

3012 the state from the previous line (this is done in PygmentsHighlighter, 

3013 below), and use it to continue processing the current line. 

3014 """ 

3015 pos = 0 

3016 tokendefs = self._tokens 

3017 if hasattr(self, '_saved_state_stack'): 

3018 statestack = list(self._saved_state_stack) 

3019 else: 

3020 statestack = list(stack) 

3021 # Fix #1113... 

3022 try: 

3023 statetokens = tokendefs[statestack[-1]] 

3024 except Exception: 

3025 # g.es_exception() 

3026 return 

3027 while 1: 

3028 for rexmatch, action, new_state in statetokens: 

3029 m = rexmatch(text, pos) 

3030 if m: 

3031 if action is not None: 

3032 # pylint: disable=unidiomatic-typecheck 

3033 # EKR: Why not use isinstance? 

3034 if type(action) is _TokenType: 

3035 yield pos, action, m.group() 

3036 else: 

3037 for item in action(self, m): 

3038 yield item 

3039 pos = m.end() 

3040 if new_state is not None: 

3041 # state transition 

3042 if isinstance(new_state, tuple): 

3043 for state in new_state: 

3044 if state == '#pop': 

3045 statestack.pop() 

3046 elif state == '#push': 

3047 statestack.append(statestack[-1]) 

3048 else: 

3049 statestack.append(state) 

3050 elif isinstance(new_state, int): 

3051 # pop 

3052 del statestack[new_state:] 

3053 elif new_state == '#push': 

3054 statestack.append(statestack[-1]) 

3055 else: 

3056 assert False, f"wrong state def: {new_state!r}" 

3057 statetokens = tokendefs[statestack[-1]] 

3058 break 

3059 else: 

3060 try: 

3061 if text[pos] == '\n': 

3062 # at EOL, reset state to "root" 

3063 pos += 1 

3064 statestack = ['root'] 

3065 statetokens = tokendefs['root'] 

3066 yield pos, Text, '\n' 

3067 continue 

3068 yield pos, Error, text[pos] 

3069 pos += 1 

3070 except IndexError: 

3071 break 

3072 self._saved_state_stack = list(statestack) 

3073 

3074 # Monkeypatch! 

3075 

3076 if pygments: 

3077 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed 

3078 #@+node:ekr.20190320062624.3: *3* class PygmentsBlockUserData(QTextBlockUserData) 

3079 # Copyright (c) Jupyter Development Team. 

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

3081 

3082 if QtGui: 

3083 

3084 

3085 class PygmentsBlockUserData(QtGui.QTextBlockUserData): # type:ignore 

3086 """ Storage for the user data associated with each line.""" 

3087 

3088 syntax_stack = ('root',) 

3089 

3090 def __init__(self, **kwds): 

3091 for key, value in kwds.items(): 

3092 setattr(self, key, value) 

3093 super().__init__() 

3094 

3095 def __repr__(self): 

3096 attrs = ['syntax_stack'] 

3097 kwds = ', '.join([ 

3098 f"{attr}={getattr(self, attr)!r}" 

3099 for attr in attrs 

3100 ]) 

3101 return f"PygmentsBlockUserData({kwds})" 

3102 #@-others 

3103#@-others 

3104#@@language python 

3105#@@tabwidth -4 

3106#@@pagewidth 70 

3107#@-leo