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

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20140827092102.18574: * @file leoColorizer.py
4#@@first
5"""All colorizing code for Leo."""
7# Indicated code are copyright (c) Jupyter Development Team.
8# Distributed under the terms of the Modified BSD License.
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
26from leo.core.leoColor import leo_color_database
27#
28# Qt imports. May fail from the bridge.
29try: # #1973
30 from leo.core.leoQt import Qsci, QtGui, QtWidgets
31 from leo.core.leoQt import UnderlineStyle, Weight # #2330
32except Exception:
33 Qsci = QtGui = QtWidgets = None
34 UnderlineStyle = Weight = None
35#@-<< imports >>
36#@+others
37#@+node:ekr.20190323044524.1: ** function: make_colorizer
38def make_colorizer(c, widget, wrapper):
39 """Return an instance of JEditColorizer or PygmentsColorizer."""
40 use_pygments = pygments and c.config.getBool('use-pygments', default=False)
41 if use_pygments:
42 return PygmentsColorizer(c, widget, wrapper)
43 return JEditColorizer(c, widget, wrapper)
44#@+node:ekr.20170127141855.1: ** class BaseColorizer
45class BaseColorizer:
46 """The base class for all Leo colorizers."""
47 #@+others
48 #@+node:ekr.20190324044744.1: *3* bc.__init__
49 def __init__(self, c, widget=None, wrapper=None):
50 """ctor for BaseColorizer class."""
51 #
52 # Copy args...
53 self.c = c
54 self.widget = widget
55 if widget:
56 # #503: widget may be None during unit tests.
57 widget.leo_colorizer = self
58 self.wrapper = wrapper
59 # This assert is not true when using multiple body editors
60 # assert(wrapper == self.c.frame.body.wrapper)
61 #
62 # Common state ivars...
63 self.enabled = False
64 # Per-node enable/disable flag.
65 # Set by updateSyntaxColorer.
66 self.highlighter = g.NullObject()
67 # May be overridden in subclass...
68 self.language = 'python'
69 # set by scanLanguageDirectives.
70 self.showInvisibles = False
71 #
72 # Statistics....
73 self.count = 0
74 self.full_recolor_count = 0
75 # For unit tests.
76 self.recolorCount = 0
77 #
78 # For traces...
79 self.matcher_name = ''
80 self.delegate_name = ''
81 #@+node:ekr.20190324045134.1: *3* bc.init
82 def init(self, p):
83 """May be over-ridden in subclasses."""
84 pass
85 #@+node:ekr.20170127142001.1: *3* bc.updateSyntaxColorer & helpers
86 # Note: these are used by unit tests.
88 def updateSyntaxColorer(self, p):
89 """
90 Scan for color directives in p and its ancestors.
91 Return True unless an coloring is unambiguously disabled.
92 Called from Leo's node-selection logic and from the colorizer.
93 """
94 if p: # This guard is required.
95 try:
96 self.enabled = self.useSyntaxColoring(p)
97 self.language = self.scanLanguageDirectives(p)
98 except Exception:
99 g.es_print('unexpected exception in updateSyntaxColorer')
100 g.es_exception()
101 #@+node:ekr.20170127142001.2: *4* bjc.scanLanguageDirectives
102 def scanLanguageDirectives(self, p):
103 """Return language based on the directives in p's ancestors."""
104 c = self.c
105 language = g.getLanguageFromAncestorAtFileNode(p)
106 return language or c.target_language
107 #@+node:ekr.20170127142001.7: *4* bjc.useSyntaxColoring & helper
108 def useSyntaxColoring(self, p):
109 """True if p's parents enable coloring in p."""
110 # Special cases for the selected node.
111 d = self.findColorDirectives(p)
112 if 'killcolor' in d:
113 return False
114 if 'nocolor-node' in d:
115 return False
116 # Now look at the parents.
117 for p in p.parents():
118 d = self.findColorDirectives(p)
119 # @killcolor anywhere disables coloring.
120 if 'killcolor' in d:
121 return False
122 # unambiguous @color enables coloring.
123 if 'color' in d and 'nocolor' not in d:
124 return True
125 # Unambiguous @nocolor disables coloring.
126 if 'nocolor' in d and 'color' not in d:
127 return False
128 return True
129 #@+node:ekr.20170127142001.8: *5* bjc.findColorDirectives
130 # Order is important: put longest matches first.
131 color_directives_pat = re.compile(
132 r'(^@color|^@killcolor|^@nocolor-node|^@nocolor)'
133 , re.MULTILINE)
135 def findColorDirectives(self, p):
136 """Return a dict with each color directive in p.b, without the leading '@'."""
137 d = {}
138 for m in self.color_directives_pat.finditer(p.b):
139 word = m.group(0)[1:]
140 d[word] = word
141 return d
142 #@-others
143#@+node:ekr.20190324115354.1: ** class BaseJEditColorizer (BaseColorizer)
144class BaseJEditColorizer(BaseColorizer):
145 """A class containing common JEdit tags machinery."""
146 # No need for a ctor.
147 #@+others
148 #@+node:ekr.20110605121601.18576: *3* bjc.addImportedRules
149 def addImportedRules(self, mode, rulesDict, rulesetName):
150 """Append any imported rules at the end of the rulesets specified in mode.importDict"""
151 if self.importedRulesets.get(rulesetName):
152 return
153 self.importedRulesets[rulesetName] = True
154 names = mode.importDict.get(
155 rulesetName, []) if hasattr(mode, 'importDict') else []
156 for name in names:
157 savedBunch = self.modeBunch
158 ok = self.init_mode(name)
159 if ok:
160 rulesDict2 = self.rulesDict
161 for key in rulesDict2.keys():
162 aList = self.rulesDict.get(key, [])
163 aList2 = rulesDict2.get(key)
164 if aList2:
165 # Don't add the standard rules again.
166 rules = [z for z in aList2 if z not in aList]
167 if rules:
168 aList.extend(rules)
169 self.rulesDict[key] = aList
170 self.initModeFromBunch(savedBunch)
171 #@+node:ekr.20110605121601.18577: *3* bjc.addLeoRules
172 def addLeoRules(self, theDict):
173 """Put Leo-specific rules to theList."""
174 # pylint: disable=no-member
175 table = [
176 # Rules added at front are added in **reverse** order.
177 ('@', self.match_leo_keywords, True), # Called after all other Leo matchers.
178 # Debatable: Leo keywords override langauge keywords.
179 ('@', self.match_at_color, True),
180 ('@', self.match_at_killcolor, True),
181 ('@', self.match_at_language, True), # 2011/01/17
182 ('@', self.match_at_nocolor, True),
183 ('@', self.match_at_nocolor_node, True),
184 ('@', self.match_at_wrap, True), # 2015/06/22
185 ('@', self.match_doc_part, True),
186 ('f', self.match_url_f, True),
187 ('g', self.match_url_g, True),
188 ('h', self.match_url_h, True),
189 ('m', self.match_url_m, True),
190 ('n', self.match_url_n, True),
191 ('p', self.match_url_p, True),
192 ('t', self.match_url_t, True),
193 ('u', self.match_unl, True),
194 ('w', self.match_url_w, True),
195 # ('<', self.match_image, True),
196 ('<', self.match_section_ref, True), # Called **first**.
197 # Rules added at back are added in normal order.
198 (' ', self.match_blanks, False),
199 ('\t', self.match_tabs, False),
200 ]
201 if self.c.config.getBool("color-trailing-whitespace"):
202 table += [
203 (' ', self.match_trailing_ws, True),
204 ('\t', self.match_trailing_ws, True),
205 ]
206 for ch, rule, atFront, in table:
207 # Replace the bound method by an unbound method.
208 rule = rule.__func__
209 theList = theDict.get(ch, [])
210 if rule not in theList:
211 if atFront:
212 theList.insert(0, rule)
213 else:
214 theList.append(rule)
215 theDict[ch] = theList
216 #@+node:ekr.20111024091133.16702: *3* bjc.configure_hard_tab_width
217 def configure_hard_tab_width(self, font):
218 """
219 Set the width of a hard tab.
221 Qt does not appear to have the required methods. Indeed,
222 https://stackoverflow.com/questions/13027091/how-to-override-tab-width-in-qt
223 assumes that QTextEdit's have only a single font(!).
225 This method probabably only works probably if the body text contains
226 a single @language directive, and it may not work properly even then.
227 """
228 c, widget = self.c, self.widget
229 if isinstance(widget, QtWidgets.QTextEdit):
230 # #1919: https://forum.qt.io/topic/99371/how-to-set-tab-stop-width-and-space-width
231 fm = QtGui.QFontMetrics(font)
232 try: # fm.horizontalAdvance
233 width = fm.horizontalAdvance(' ') * abs(c.tab_width)
234 widget.setTabStopDistance(width)
235 except Exception:
236 width = fm.width(' ') * abs(c.tab_width)
237 widget.setTabStopWidth(width) # Obsolete.
238 else:
239 # To do: configure the QScintilla widget.
240 pass
241 #@+node:ekr.20110605121601.18578: *3* bjc.configure_tags & helpers
242 def configure_tags(self):
243 """Configure all tags."""
244 wrapper = self.wrapper
245 if wrapper and hasattr(wrapper, 'start_tag_configure'):
246 wrapper.start_tag_configure()
247 self.configure_fonts()
248 self.configure_colors()
249 self.configure_variable_tags()
250 if wrapper and hasattr(wrapper, 'end_tag_configure'):
251 wrapper.end_tag_configure()
252 #@+node:ekr.20190324172632.1: *4* bjc.configure_colors
253 def configure_colors(self):
254 """Configure all colors in the default colors dict."""
255 c, wrapper = self.c, self.wrapper
256 getColor = c.config.getColor
257 # getColor puts the color name in standard form:
258 # color = color.replace(' ', '').lower().strip()
259 for key in sorted(self.default_colors_dict.keys()):
260 option_name, default_color = self.default_colors_dict[key]
261 color = (
262 getColor(f"{self.language}_{option_name}") or
263 getColor(option_name) or
264 default_color
265 )
266 # Must use foreground, not fg.
267 try:
268 wrapper.tag_configure(key, foreground=color)
269 except Exception: # Recover after a user settings error.
270 g.es_exception()
271 wrapper.tag_configure(key, foreground=default_color)
272 #@+node:ekr.20190324172242.1: *4* bjc.configure_fonts & helper
273 def configure_fonts(self):
274 """Configure all fonts in the default fonts dict."""
275 c = self.c
276 isQt = g.app.gui.guiName().startswith('qt')
277 wrapper = self.wrapper
278 #
279 # Get the default body font.
280 defaultBodyfont = self.fonts.get('default_body_font')
281 if not defaultBodyfont:
282 defaultBodyfont = c.config.getFontFromParams(
283 "body_text_font_family", "body_text_font_size",
284 "body_text_font_slant", "body_text_font_weight",
285 c.config.defaultBodyFontSize)
286 self.fonts['default_body_font'] = defaultBodyfont
287 #
288 # Set all fonts.
289 for key in sorted(self.default_font_dict.keys()):
290 option_name = self.default_font_dict[key]
291 # Find language specific setting before general setting.
292 table = (
293 f"{self.language}_{option_name}",
294 option_name,
295 )
296 for name in table:
297 font = self.fonts.get(name)
298 if font:
299 break
300 font = self.find_font(key, name)
301 if font:
302 self.fonts[key] = font
303 wrapper.tag_configure(key, font=font)
304 if isQt and key == 'url':
305 font.setUnderline(True)
306 # #1919: This really isn't correct.
307 self.configure_hard_tab_width(font)
308 break
309 else:
310 # Neither setting exists.
311 self.fonts[key] = None # Essential
312 wrapper.tag_configure(key, font=defaultBodyfont)
313 #@+node:ekr.20190326034006.1: *5* bjc.find_font
314 zoom_dict: Dict[str, int] = {}
315 # Keys are key::settings_names, values are cumulative font size.
317 def find_font(self, key, setting_name):
318 """
319 Return the font for the given setting name.
320 """
321 trace = 'zoom' in g.app.debug
322 c, get = self.c, self.c.config.get
323 default_size = c.config.defaultBodyFontSize
324 for name in (setting_name, setting_name.rstrip('_font')):
325 size_error = False
326 family = get(name + '_family', 'family')
327 size = get(name + '_size', 'size')
328 slant = get(name + '_slant', 'slant')
329 weight = get(name + '_weight', 'weight')
330 if family or slant or weight or size:
331 family = family or g.app.config.defaultFontFamily
332 key = f"{key}::{setting_name}"
333 if key in self.zoom_dict:
334 old_size = self.zoom_dict.get(key)
335 else:
336 # It's a good idea to set size explicitly.
337 old_size = size or default_size
338 if isinstance(old_size, str):
339 # All settings should be in units of points.
340 try:
341 if old_size.endswith(('pt', 'px'),):
342 old_size = int(old_size[:-2])
343 else:
344 old_size = int(old_size)
345 except ValueError:
346 size_error = True
347 elif not isinstance(old_size, int):
348 size_error = True
349 if size_error:
350 g.trace('bad old_size:', old_size.__class__, old_size)
351 size = old_size
352 else:
353 # #490: Use c.zoom_size if it exists.
354 zoom_delta = getattr(c, 'zoom_delta', 0)
355 if zoom_delta:
356 size = old_size + zoom_delta
357 self.zoom_dict[key] = size
358 slant = slant or 'roman'
359 weight = weight or 'normal'
360 size = str(size)
361 font = g.app.gui.getFontFromParams(family, size, slant, weight)
362 # A good trace: the key shows what is happening.
363 if font:
364 if trace:
365 g.trace(
366 f"key: {key:>35} family: {family or 'None'} "
367 f"size: {size or 'None'} {slant} {weight}")
368 return font
369 return None
370 #@+node:ekr.20110605121601.18579: *4* bjc.configure_variable_tags
371 def configure_variable_tags(self):
372 c = self.c
373 wrapper = self.wrapper
374 wrapper.tag_configure("link", underline=0)
375 use_pygments = pygments and c.config.getBool('use-pygments', default=False)
376 name = 'name.other' if use_pygments else 'name'
377 wrapper.tag_configure(name, underline=1 if self.underline_undefined else 0)
378 for name, option_name, default_color in (
379 # ("blank", "show_invisibles_space_background_color", "Gray90"),
380 # ("tab", "show_invisibles_tab_background_color", "Gray80"),
381 ("elide", None, "yellow"),
382 ):
383 if self.showInvisibles:
384 color = c.config.getColor(option_name) if option_name else default_color
385 else:
386 option_name, default_color = self.default_colors_dict.get(
387 name, (None, None),)
388 color = c.config.getColor(option_name) if option_name else ''
389 try:
390 wrapper.tag_configure(name, background=color)
391 except Exception: # A user error.
392 wrapper.tag_configure(name, background=default_color)
393 g.es_print(f"invalid setting: {name!r} = {default_color!r}")
394 # Special case:
395 if not self.showInvisibles:
396 wrapper.tag_configure("elide", elide="1")
397 #@+node:ekr.20110605121601.18574: *3* bjc.defineDefaultColorsDict
398 #@@nobeautify
400 def defineDefaultColorsDict (self):
402 # These defaults are sure to exist.
403 self.default_colors_dict = {
404 #
405 # Used in Leo rules...
406 # tag name :( option name, default color),
407 'blank' :('show_invisibles_space_color', '#E5E5E5'), # gray90
408 'docpart' :('doc_part_color', 'red'),
409 'leokeyword' :('leo_keyword_color', 'blue'),
410 'link' :('section_name_color', 'red'),
411 'name' :('undefined_section_name_color','red'),
412 'namebrackets' :('section_name_brackets_color', 'blue'),
413 'tab' :('show_invisibles_tab_color', '#CCCCCC'), # gray80
414 'url' :('url_color', 'purple'),
415 #
416 # Pygments tags. Non-default values are taken from 'default' style.
417 #
418 # Top-level...
419 # tag name :( option name, default color),
420 'error' :('error', '#FF0000'), # border
421 'other' :('other', 'white'),
422 'punctuation' :('punctuation', 'white'),
423 'whitespace' :('whitespace', '#bbbbbb'),
424 'xt' :('xt', '#bbbbbb'),
425 #
426 # Comment...
427 # tag name :( option name, default color),
428 'comment' :('comment', '#408080'), # italic
429 'comment.hashbang' :('comment.hashbang', '#408080'),
430 'comment.multiline' :('comment.multiline', '#408080'),
431 'comment.special' :('comment.special', '#408080'),
432 'comment.preproc' :('comment.preproc', '#BC7A00'), # noitalic
433 'comment.single' :('comment.single', '#BC7A00'), # italic
434 #
435 # Generic...
436 # tag name :( option name, default color),
437 'generic' :('generic', '#A00000'),
438 'generic.deleted' :('generic.deleted', '#A00000'),
439 'generic.emph' :('generic.emph', '#000080'), # italic
440 'generic.error' :('generic.error', '#FF0000'),
441 'generic.heading' :('generic.heading', '#000080'), # bold
442 'generic.inserted' :('generic.inserted', '#00A000'),
443 'generic.output' :('generic.output', '#888'),
444 'generic.prompt' :('generic.prompt', '#000080'), # bold
445 'generic.strong' :('generic.strong', '#000080'), # bold
446 'generic.subheading':('generic.subheading', '#800080'), # bold
447 'generic.traceback' :('generic.traceback', '#04D'),
448 #
449 # Keyword...
450 # tag name :( option name, default color),
451 'keyword' :('keyword', '#008000'), # bold
452 'keyword.constant' :('keyword.constant', '#008000'),
453 'keyword.declaration' :('keyword.declaration', '#008000'),
454 'keyword.namespace' :('keyword.namespace', '#008000'),
455 'keyword.pseudo' :('keyword.pseudo', '#008000'), # nobold
456 'keyword.reserved' :('keyword.reserved', '#008000'),
457 'keyword.type' :('keyword.type', '#B00040'),
458 #
459 # Literal...
460 # tag name :( option name, default color),
461 'literal' :('literal', 'white'),
462 'literal.date' :('literal.date', 'white'),
463 #
464 # Name...
465 # tag name :( option name, default color
466 # 'name' defined below.
467 'name.attribute' :('name.attribute', '#7D9029'), # bold
468 'name.builtin' :('name.builtin', '#008000'),
469 'name.builtin.pseudo' :('name.builtin.pseudo','#008000'),
470 'name.class' :('name.class', '#0000FF'), # bold
471 'name.constant' :('name.constant', '#880000'),
472 'name.decorator' :('name.decorator', '#AA22FF'),
473 'name.entity' :('name.entity', '#999999'), # bold
474 'name.exception' :('name.exception', '#D2413A'), # bold
475 'name.function' :('name.function', '#0000FF'),
476 'name.function.magic' :('name.function.magic','#0000FF'),
477 'name.label' :('name.label', '#A0A000'),
478 'name.namespace' :('name.namespace', '#0000FF'), # bold
479 'name.other' :('name.other', 'red'),
480 'name.pygments' :('name.pygments', 'white'),
481 # A hack: getLegacyFormat returns name.pygments instead of name.
482 'name.tag' :('name.tag', '#008000'), # bold
483 'name.variable' :('name.variable', '#19177C'),
484 'name.variable.class' :('name.variable.class', '#19177C'),
485 'name.variable.global' :('name.variable.global', '#19177C'),
486 'name.variable.instance':('name.variable.instance', '#19177C'),
487 'name.variable.magic' :('name.variable.magic', '#19177C'),
488 #
489 # Number...
490 # tag name :( option name, default color
491 'number' :('number', '#666666'),
492 'number.bin' :('number.bin', '#666666'),
493 'number.float' :('number.float', '#666666'),
494 'number.hex' :('number.hex', '#666666'),
495 'number.integer' :('number.integer', '#666666'),
496 'number.integer.long' :('number.integer.long','#666666'),
497 'number.oct' :('number.oct', '#666666'),
498 #
499 # Operator...
500 # tag name :( option name, default color
501 # 'operator' defined below.
502 'operator.word' :('operator.Word', '#AA22FF'), # bold
503 #
504 # String...
505 # tag name :( option name, default color
506 'string' :('string', '#BA2121'),
507 'string.affix' :('string.affix', '#BA2121'),
508 'string.backtick' :('string.backtick', '#BA2121'),
509 'string.char' :('string.char', '#BA2121'),
510 'string.delimiter' :('string.delimiter', '#BA2121'),
511 'string.doc' :('string.doc', '#BA2121'), # italic
512 'string.double' :('string.double', '#BA2121'),
513 'string.escape' :('string.escape', '#BB6622'), # bold
514 'string.heredoc' :('string.heredoc', '#BA2121'),
515 'string.interpol' :('string.interpol', '#BB6688'), # bold
516 'string.other' :('string.other', '#008000'),
517 'string.regex' :('string.regex', '#BB6688'),
518 'string.single' :('string.single', '#BA2121'),
519 'string.symbol' :('string.symbol', '#19177C'),
520 #
521 # jEdit tags.
522 # tag name :( option name, default color),
523 'comment1' :('comment1_color', 'red'),
524 'comment2' :('comment2_color', 'red'),
525 'comment3' :('comment3_color', 'red'),
526 'comment4' :('comment4_color', 'red'),
527 'function' :('function_color', 'black'),
528 'keyword1' :('keyword1_color', 'blue'),
529 'keyword2' :('keyword2_color', 'blue'),
530 'keyword3' :('keyword3_color', 'blue'),
531 'keyword4' :('keyword4_color', 'blue'),
532 'keyword5' :('keyword5_color', 'blue'),
533 'label' :('label_color', 'black'),
534 'literal1' :('literal1_color', '#00aa00'),
535 'literal2' :('literal2_color', '#00aa00'),
536 'literal3' :('literal3_color', '#00aa00'),
537 'literal4' :('literal4_color', '#00aa00'),
538 'markup' :('markup_color', 'red'),
539 'null' :('null_color', None), #'black'),
540 'operator' :('operator_color', 'black'),
541 'trailing_whitespace': ('trailing_whitespace_color', '#808080'),
542 }
543 #@+node:ekr.20110605121601.18575: *3* bjc.defineDefaultFontDict
544 #@@nobeautify
546 def defineDefaultFontDict (self):
548 self.default_font_dict = {
549 #
550 # Used in Leo rules...
551 # tag name : option name
552 'blank' :'show_invisibles_space_font', # 2011/10/24.
553 'docpart' :'doc_part_font',
554 'leokeyword' :'leo_keyword_font',
555 'link' :'section_name_font',
556 'name' :'undefined_section_name_font',
557 'namebrackets' :'section_name_brackets_font',
558 'tab' :'show_invisibles_tab_font', # 2011/10/24.
559 'url' :'url_font',
560 #
561 # Pygments tags (lower case)...
562 # tag name : option name
563 "comment" :'comment1_font',
564 "comment.preproc" :'comment2_font',
565 "comment.single" :'comment1_font',
566 "error" :'null_font',
567 "generic.deleted" :'literal4_font',
568 "generic.emph" :'literal4_font',
569 "generic.error" :'literal4_font',
570 "generic.heading" :'literal4_font',
571 "generic.inserted" :'literal4_font',
572 "generic.output" :'literal4_font',
573 "generic.prompt" :'literal4_font',
574 "generic.strong" :'literal4_font',
575 "generic.subheading":'literal4_font',
576 "generic.traceback" :'literal4_font',
577 "keyword" :'keyword1_font',
578 "keyword.pseudo" :'keyword2_font',
579 "keyword.type" :'keyword3_font',
580 "name.attribute" :'null_font',
581 "name.builtin" :'null_font',
582 "name.class" :'null_font',
583 "name.constant" :'null_font',
584 "name.decorator" :'null_font',
585 "name.entity" :'null_font',
586 "name.exception" :'null_font',
587 "name.function" :'null_font',
588 "name.label" :'null_font',
589 "name.namespace" :'null_font',
590 "name.tag" :'null_font',
591 "name.variable" :'null_font',
592 "number" :'null_font',
593 "operator.word" :'keyword4_font',
594 "string" :'literal1_font',
595 "string.doc" :'literal1_font',
596 "string.escape" :'literal1_font',
597 "string.interpol" :'literal1_font',
598 "string.other" :'literal1_font',
599 "string.regex" :'literal1_font',
600 "string.single" :'literal1_font',
601 "string.symbol" :'literal1_font',
602 'xt' :'text_font',
603 "whitespace" :'text_font',
604 #
605 # jEdit tags.
606 # tag name : option name
607 'comment1' :'comment1_font',
608 'comment2' :'comment2_font',
609 'comment3' :'comment3_font',
610 'comment4' :'comment4_font',
611 #'default' :'default_font',
612 'function' :'function_font',
613 'keyword1' :'keyword1_font',
614 'keyword2' :'keyword2_font',
615 'keyword3' :'keyword3_font',
616 'keyword4' :'keyword4_font',
617 'keyword5' :'keyword5_font',
618 'label' :'label_font',
619 'literal1' :'literal1_font',
620 'literal2' :'literal2_font',
621 'literal3' :'literal3_font',
622 'literal4' :'literal4_font',
623 'markup' :'markup_font',
624 # 'nocolor' This tag is used, but never generates code.
625 'null' :'null_font',
626 'operator' :'operator_font',
627 'trailing_whitespace' :'trailing_whitespace_font',
628 }
629 #@+node:ekr.20110605121601.18573: *3* bjc.defineLeoKeywordsDict
630 def defineLeoKeywordsDict(self):
631 self.leoKeywordsDict = {}
632 for key in g.globalDirectiveList:
633 self.leoKeywordsDict[key] = 'leokeyword'
634 #@+node:ekr.20170514054524.1: *3* bjc.getFontFromParams
635 def getFontFromParams(self, family, size, slant, weight, defaultSize=12):
636 return None
638 # def setFontFromConfig(self):
639 # pass
640 #@+node:ekr.20110605121601.18581: *3* bjc.init_mode & helpers
641 def init_mode(self, name):
642 """Name may be a language name or a delegate name."""
643 if not name:
644 return False
645 if name == 'latex':
646 name = 'tex'
647 # #1088: use tex mode for both tex and latex.
648 language, rulesetName = self.nameToRulesetName(name)
649 if 'coloring' in g.app.debug and not g.unitTesting:
650 print(f"language: {language!r}, rulesetName: {rulesetName!r}")
651 bunch = self.modes.get(rulesetName)
652 if bunch:
653 if bunch.language == 'unknown-language':
654 return False
655 self.initModeFromBunch(bunch)
656 self.language = language # 2011/05/30
657 return True
658 # Don't try to import a non-existent language.
659 path = g.os_path_join(g.app.loadDir, '..', 'modes')
660 fn = g.os_path_join(path, f"{language}.py")
661 if g.os_path_exists(fn):
662 mode = g.import_module(name=f"leo.modes.{language}")
663 else:
664 mode = None
665 return self.init_mode_from_module(name, mode)
666 #@+node:btheado.20131124162237.16303: *4* bjc.init_mode_from_module
667 def init_mode_from_module(self, name, mode):
668 """
669 Name may be a language name or a delegate name.
670 Mode is a python module or class containing all
671 coloring rule attributes for the mode.
672 """
673 language, rulesetName = self.nameToRulesetName(name)
674 if mode:
675 # A hack to give modes/forth.py access to c.
676 if hasattr(mode, 'pre_init_mode'):
677 mode.pre_init_mode(self.c)
678 else:
679 # Create a dummy bunch to limit recursion.
680 self.modes[rulesetName] = self.modeBunch = g.Bunch(
681 attributesDict={},
682 defaultColor=None,
683 keywordsDict={},
684 language='unknown-language',
685 mode=mode,
686 properties={},
687 rulesDict={},
688 rulesetName=rulesetName,
689 word_chars=self.word_chars, # 2011/05/21
690 )
691 self.rulesetName = rulesetName
692 self.language = 'unknown-language'
693 return False
694 self.language = language
695 self.rulesetName = rulesetName
696 self.properties = getattr(mode, 'properties', None) or {}
697 #
698 # #1334: Careful: getattr(mode, ivar, {}) might be None!
699 #
700 d: Dict[Any, Any] = getattr(mode, 'keywordsDictDict', {}) or {}
701 self.keywordsDict = d.get(rulesetName, {})
702 self.setKeywords()
703 d = getattr(mode, 'attributesDictDict', {}) or {}
704 self.attributesDict: Dict[str, Any] = d.get(rulesetName, {})
705 self.setModeAttributes()
706 d = getattr(mode, 'rulesDictDict', {}) or {}
707 self.rulesDict: Dict[str, Any] = d.get(rulesetName, {})
708 self.addLeoRules(self.rulesDict)
709 self.defaultColor = 'null'
710 self.mode = mode
711 self.modes[rulesetName] = self.modeBunch = g.Bunch(
712 attributesDict=self.attributesDict,
713 defaultColor=self.defaultColor,
714 keywordsDict=self.keywordsDict,
715 language=self.language,
716 mode=self.mode,
717 properties=self.properties,
718 rulesDict=self.rulesDict,
719 rulesetName=self.rulesetName,
720 word_chars=self.word_chars, # 2011/05/21
721 )
722 # Do this after 'officially' initing the mode, to limit recursion.
723 self.addImportedRules(mode, self.rulesDict, rulesetName)
724 self.updateDelimsTables()
725 initialDelegate = self.properties.get('initialModeDelegate')
726 if initialDelegate:
727 # Replace the original mode by the delegate mode.
728 self.init_mode(initialDelegate)
729 language2, rulesetName2 = self.nameToRulesetName(initialDelegate)
730 self.modes[rulesetName] = self.modes.get(rulesetName2)
731 self.language = language2 # 2017/01/31
732 else:
733 self.language = language # 2017/01/31
734 return True
735 #@+node:ekr.20110605121601.18582: *4* bjc.nameToRulesetName
736 def nameToRulesetName(self, name):
737 """
738 Compute language and rulesetName from name, which is either a language
739 name or a delegate name.
740 """
741 if not name:
742 return ''
743 name = name.lower()
744 # #1334. Lower-case the name, regardless of the spelling in @language.
745 i = name.find('::')
746 if i == -1:
747 language = name
748 # New in Leo 5.0: allow delegated language names.
749 language = g.app.delegate_language_dict.get(language, language)
750 rulesetName = f"{language}_main"
751 else:
752 language = name[:i]
753 delegate = name[i + 2 :]
754 rulesetName = self.munge(f"{language}_{delegate}")
755 return language, rulesetName
756 #@+node:ekr.20110605121601.18583: *4* bjc.setKeywords
757 def setKeywords(self):
758 """
759 Initialize the keywords for the present language.
761 Set self.word_chars ivar to string.letters + string.digits
762 plus any other character appearing in any keyword.
763 """
764 # Add any new user keywords to leoKeywordsDict.
765 d = self.keywordsDict
766 keys = list(d.keys())
767 for s in g.globalDirectiveList:
768 key = '@' + s
769 if key not in keys:
770 d[key] = 'leokeyword'
771 # Create a temporary chars list. It will be converted to a dict later.
772 chars = [z for z in string.ascii_letters + string.digits]
773 for key in list(d.keys()):
774 for ch in key:
775 if ch not in chars:
776 chars.append(g.checkUnicode(ch))
777 # jEdit2Py now does this check, so this isn't really needed.
778 # But it is needed for forth.py.
779 for ch in (' ', '\t'):
780 if ch in chars:
781 # g.es_print('removing %s from word_chars' % (repr(ch)))
782 chars.remove(ch)
783 # Convert chars to a dict for faster access.
784 self.word_chars: Dict[str, str] = {}
785 for z in chars:
786 self.word_chars[z] = z
787 #@+node:ekr.20110605121601.18584: *4* bjc.setModeAttributes
788 def setModeAttributes(self):
789 """
790 Set the ivars from self.attributesDict,
791 converting 'true'/'false' to True and False.
792 """
793 d = self.attributesDict
794 aList = (
795 ('default', 'null'),
796 ('digit_re', ''),
797 ('escape', ''), # New in Leo 4.4.2.
798 ('highlight_digits', True),
799 ('ignore_case', True),
800 ('no_word_sep', ''),
801 )
802 for key, default in aList:
803 val = d.get(key, default)
804 if val in ('true', 'True'):
805 val = True
806 if val in ('false', 'False'):
807 val = False
808 setattr(self, key, val)
809 #@+node:ekr.20110605121601.18585: *4* bjc.initModeFromBunch
810 def initModeFromBunch(self, bunch):
811 self.modeBunch = bunch
812 self.attributesDict = bunch.attributesDict
813 self.setModeAttributes()
814 self.defaultColor = bunch.defaultColor
815 self.keywordsDict = bunch.keywordsDict
816 self.language = bunch.language
817 self.mode = bunch.mode
818 self.properties = bunch.properties
819 self.rulesDict = bunch.rulesDict
820 self.rulesetName = bunch.rulesetName
821 self.word_chars = bunch.word_chars # 2011/05/21
822 #@+node:ekr.20110605121601.18586: *4* bjc.updateDelimsTables
823 def updateDelimsTables(self):
824 """Update g.app.language_delims_dict if no entry for the language exists."""
825 d = self.properties
826 lineComment = d.get('lineComment')
827 startComment = d.get('commentStart')
828 endComment = d.get('commentEnd')
829 if lineComment and startComment and endComment:
830 delims = f"{lineComment} {startComment} {endComment}"
831 elif startComment and endComment:
832 delims = f"{startComment} {endComment}"
833 elif lineComment:
834 delims = f"{lineComment}"
835 else:
836 delims = None
837 if delims:
838 d = g.app.language_delims_dict
839 if not d.get(self.language):
840 d[self.language] = delims
841 #@+node:ekr.20190324050727.1: *3* bjc.init_style_ivars
842 def init_style_ivars(self):
843 """Init Style data common to JEdit and Pygments colorizers."""
844 # init() properly sets these for each language.
845 self.actualColorDict = {} # Used only by setTag.
846 self.hyperCount = 0
847 # Attributes dict ivars: defaults are as shown...
848 self.default = 'null'
849 self.digit_re = ''
850 self.escape = ''
851 self.highlight_digits = True
852 self.ignore_case = True
853 self.no_word_sep = ''
854 # Debugging...
855 self.allow_mark_prev = True
856 self.n_setTag = 0
857 self.tagCount = 0
858 self.trace_leo_matches = False
859 self.trace_match_flag = False
860 # Profiling...
861 self.recolorCount = 0 # Total calls to recolor
862 self.stateCount = 0 # Total calls to setCurrentState
863 self.totalStates = 0
864 self.maxStateNumber = 0
865 self.totalKeywordsCalls = 0
866 self.totalLeoKeywordsCalls = 0
867 # Mode data...
868 self.defaultRulesList = []
869 self.importedRulesets = {}
870 self.initLanguage = None
871 self.prev = None # The previous token.
872 self.fonts = {} # Keys are config names. Values are actual fonts.
873 self.keywords = {} # Keys are keywords, values are 0..5.
874 # Keys are state ints, values are language names.
875 self.modes = {} # Keys are languages, values are modes.
876 self.mode = None # The mode object for the present language.
877 self.modeBunch = None # A bunch fully describing a mode.
878 self.modeStack = []
879 self.rulesDict = {}
880 # self.defineAndExtendForthWords()
881 self.word_chars = {} # Inited by init_keywords().
882 self.tags = [
883 # 8 Leo-specific tags.
884 "blank", # show_invisibles_space_color
885 "docpart",
886 "leokeyword",
887 "link",
888 "name",
889 "namebrackets",
890 "tab", # show_invisibles_space_color
891 "url",
892 # jEdit tags.
893 'comment1', 'comment2', 'comment3', 'comment4',
894 # default, # exists, but never generated.
895 'function',
896 'keyword1', 'keyword2', 'keyword3', 'keyword4',
897 'label', 'literal1', 'literal2', 'literal3', 'literal4',
898 'markup', 'operator',
899 'trailing_whitespace',
900 ]
901 #@+node:ekr.20110605121601.18587: *3* bjc.munge
902 def munge(self, s):
903 """Munge a mode name so that it is a valid python id."""
904 valid = string.ascii_letters + string.digits + '_'
905 return ''.join([ch.lower() if ch in valid else '_' for ch in s])
906 #@+node:ekr.20171114041307.1: *3* bjc.reloadSettings & helper
907 #@@nobeautify
908 def reloadSettings(self):
909 c, getBool = self.c, self.c.config.getBool
910 #
911 # Init all settings ivars.
912 self.color_tags_list = []
913 self.showInvisibles = getBool("show-invisibles-by-default")
914 self.underline_undefined = getBool("underline-undefined-section-names")
915 self.use_hyperlinks = getBool("use-hyperlinks")
916 self.use_pygments = None # Set in report_changes.
917 self.use_pygments_styles = getBool('use-pygments-styles', default=True)
918 #
919 # Report changes to pygments settings.
920 self.report_changes()
921 #
922 # Init the default fonts.
923 self.bold_font = c.config.getFontFromParams(
924 "body_text_font_family", "body_text_font_size",
925 "body_text_font_slant", "body_text_font_weight",
926 c.config.defaultBodyFontSize)
927 self.italic_font = c.config.getFontFromParams(
928 "body_text_font_family", "body_text_font_size",
929 "body_text_font_slant", "body_text_font_weight",
930 c.config.defaultBodyFontSize)
931 self.bolditalic_font = c.config.getFontFromParams(
932 "body_text_font_family", "body_text_font_size",
933 "body_text_font_slant", "body_text_font_weight",
934 c.config.defaultBodyFontSize)
935 #@+node:ekr.20190327053604.1: *4* bjc.report_changes
936 prev_use_pygments = None
937 prev_use_styles = None
938 prev_style = None
940 def report_changes(self):
941 """Report changes to pygments settings"""
942 c = self.c
943 use_pygments = c.config.getBool('use-pygments', default=False)
944 if not use_pygments: # 1696.
945 return
946 trace = 'coloring' in g.app.debug and not g.unitTesting
947 if trace:
948 g.es_print('\nreport changes...')
950 def show(setting, val):
951 if trace:
952 g.es_print(f"{setting:35}: {val}")
954 #
955 # Set self.use_pygments only once: it can't be changed later.
956 # There is no easy way to re-instantiate classes created by make_colorizer.
957 if self.prev_use_pygments is None:
958 self.use_pygments = self.prev_use_pygments = use_pygments
959 show('@bool use-pygments', use_pygments)
960 elif use_pygments == self.prev_use_pygments:
961 show('@bool use-pygments', use_pygments)
962 else:
963 g.es_print(
964 f"{'Can not change @bool use-pygments':35}: "
965 f"{self.prev_use_pygments}",
966 color='red')
967 #
968 # Report everything if we are tracing.
969 style_name = c.config.getString('pygments-style-name') or 'default'
970 # Don't set an ivar. It's not used in this class.
971 # This setting is used only in the LeoHighlighter class
972 show('@bool use-pytments-styles', self.use_pygments_styles)
973 show('@string pygments-style-name', style_name)
974 #
975 # Report changes to @bool use-pygments-style
976 if self.prev_use_styles is None:
977 self.prev_use_styles = self.use_pygments_styles
978 elif self.use_pygments_styles != self.prev_use_styles:
979 g.es_print(f"using pygments styles: {self.use_pygments_styles}")
980 #
981 # Report @string pygments-style-name only if we are using styles.
982 if not self.use_pygments_styles:
983 return
984 #
985 # Report changes to @string pygments-style-name
986 if self.prev_style is None:
987 self.prev_style = style_name
988 elif style_name != self.prev_style:
989 g.es_print(f"New pygments style: {style_name}")
990 self.prev_style = style_name
991 #@+node:ekr.20110605121601.18641: *3* bjc.setTag
992 last_v = None
994 def setTag(self, tag, s, i, j):
995 """Set the tag in the highlighter."""
996 trace = 'coloring' in g.app.debug and not g.unitTesting
997 self.n_setTag += 1
998 if i == j:
999 return
1000 wrapper = self.wrapper # A QTextEditWrapper
1001 if not tag.strip():
1002 return
1003 tag = tag.lower().strip()
1004 # A hack to allow continuation dots on any tag.
1005 dots = tag.startswith('dots')
1006 if dots:
1007 tag = tag[len('dots') :]
1008 colorName = wrapper.configDict.get(tag)
1009 # This color name should already be valid.
1010 if not colorName:
1011 return
1012 #
1013 # New in Leo 5.8.1: allow symbolic color names here.
1014 # This now works because all keys in leo_color_database are normalized.
1015 colorName = colorName.replace(
1016 ' ', '').replace('-', '').replace('_', '').lower().strip()
1017 colorName = leo_color_database.get(colorName, colorName)
1018 # Get the actual color.
1019 color = self.actualColorDict.get(colorName)
1020 if not color:
1021 color = QtGui.QColor(colorName)
1022 if color.isValid():
1023 self.actualColorDict[colorName] = color
1024 else:
1025 g.trace('unknown color name', colorName, g.callers())
1026 return
1027 underline = wrapper.configUnderlineDict.get(tag)
1028 format = QtGui.QTextCharFormat()
1029 font = self.fonts.get(tag)
1030 if font:
1031 format.setFont(font)
1032 self.configure_hard_tab_width(font) # #1919.
1033 if tag in ('blank', 'tab'):
1034 if tag == 'tab' or colorName == 'black':
1035 format.setFontUnderline(True)
1036 if colorName != 'black':
1037 format.setBackground(color)
1038 elif underline:
1039 format.setForeground(color)
1040 format.setUnderlineStyle(UnderlineStyle.SingleUnderline)
1041 format.setFontUnderline(True)
1042 elif dots or tag == 'trailing_whitespace':
1043 format.setForeground(color)
1044 format.setUnderlineStyle(UnderlineStyle.DotLine)
1045 else:
1046 format.setForeground(color)
1047 format.setUnderlineStyle(UnderlineStyle.NoUnderline)
1048 self.tagCount += 1
1049 if trace:
1050 # A superb trace.
1051 if len(repr(s[i:j])) <= 20:
1052 s2 = repr(s[i:j])
1053 else:
1054 s2 = repr(s[i : i + 17 - 2] + '...')
1055 kind_s = f"{self.language}.{tag}"
1056 kind_s2 = f"{self.delegate_name}:" if self.delegate_name else ''
1057 print(
1058 f"setTag: {kind_s:25} {i:3} {j:3} {s2:>20} "
1059 f"{self.rulesetName}:{kind_s2}{self.matcher_name}"
1060 )
1061 self.highlighter.setFormat(i, j - i, format)
1062 #@-others
1063#@+node:ekr.20110605121601.18569: ** class JEditColorizer(BaseJEditColorizer)
1064# This is c.frame.body.colorizer
1067class JEditColorizer(BaseJEditColorizer):
1068 """
1069 The JEditColorizer class adapts jEdit pattern matchers for QSyntaxHighlighter.
1070 For full documentation, see:
1071 https://github.com/leo-editor/leo-editor/blob/master/leo/doc/colorizer.md
1072 """
1073 #@+others
1074 #@+node:ekr.20110605121601.18572: *3* jedit.__init__ & helpers
1075 def __init__(self, c, widget, wrapper):
1076 """Ctor for JEditColorizer class."""
1077 super().__init__(c, widget, wrapper)
1078 #
1079 # Create the highlighter. The default is NullObject.
1080 if isinstance(widget, QtWidgets.QTextEdit):
1081 self.highlighter = LeoHighlighter(c,
1082 colorizer=self,
1083 document=widget.document(),
1084 )
1085 #
1086 # State data used only by this class...
1087 self.after_doc_language = None
1088 self.initialStateNumber = -1
1089 self.old_v = None
1090 self.nextState = 1 # Dont use 0.
1091 self.n2languageDict = {-1: c.target_language}
1092 self.restartDict = {} # Keys are state numbers, values are restart functions.
1093 self.stateDict = {} # Keys are state numbers, values state names.
1094 self.stateNameDict = {} # Keys are state names, values are state numbers.
1095 # #2276: Set by init_section_delims.
1096 self.section_delim1 = '<<'
1097 self.section_delim2 = '>>'
1098 #
1099 # Init common data...
1100 self.reloadSettings()
1101 #@+node:ekr.20110605121601.18580: *4* jedit.init
1102 def init(self, p=None):
1103 """Init the colorizer, but *not* state."""
1104 #
1105 # These *must* be recomputed.
1106 self.initialStateNumber = self.setInitialStateNumber()
1107 #
1108 # Fix #389. Do *not* change these.
1109 # self.nextState = 1 # Dont use 0.
1110 # self.stateDict = {}
1111 # self.stateNameDict = {}
1112 # self.restartDict = {}
1113 self.init_mode(self.language)
1114 self.clearState()
1115 # Used by matchers.
1116 self.prev = None
1117 # Must be done to support per-language @font/@color settings.
1118 self.configure_tags()
1119 self.init_section_delims() # #2276
1120 #@+node:ekr.20170201082248.1: *4* jedit.init_all_state
1121 def init_all_state(self, v):
1122 """Completely init all state data."""
1123 assert self.language, g.callers(8)
1124 self.old_v = v
1125 self.n2languageDict = {-1: self.language}
1126 self.nextState = 1 # Dont use 0.
1127 self.restartDict = {}
1128 self.stateDict = {}
1129 self.stateNameDict = {}
1130 #@+node:ekr.20211029073553.1: *4* jedit.init_section_delims
1131 def init_section_delims(self):
1133 p = self.c.p
1135 def find_delims(v):
1136 for s in g.splitLines(v.b):
1137 m = g.g_section_delims_pat.match(s)
1138 if m:
1139 return m
1140 return None
1142 v = g.findAncestorVnodeByPredicate(p, v_predicate=find_delims)
1143 if v:
1144 m = find_delims(v)
1145 self.section_delim1 = m.group(1)
1146 self.section_delim2 = m.group(2)
1147 else:
1148 self.section_delim1 = '<<'
1149 self.section_delim2 = '>>'
1150 #@+node:ekr.20190326183005.1: *4* jedit.reloadSettings
1151 def reloadSettings(self):
1152 """Complete the initialization of all settings."""
1153 if 'coloring' in g.app.debug and not g.unitTesting:
1154 print('jedit.reloadSettings.')
1155 # Do the basic inits.
1156 BaseJEditColorizer.reloadSettings(self)
1157 # Init everything else.
1158 self.init_style_ivars()
1159 self.defineLeoKeywordsDict()
1160 self.defineDefaultColorsDict()
1161 self.defineDefaultFontDict()
1162 self.init()
1163 #@+node:ekr.20110605121601.18589: *3* jedit.Pattern matchers
1164 #@+node:ekr.20110605121601.18590: *4* About the pattern matchers
1165 #@@language rest
1166 #@+at
1167 # The following jEdit matcher methods return the length of the matched text if the
1168 # match succeeds, and zero otherwise. In most cases, these methods colorize all
1169 # the matched text.
1170 #
1171 # The following arguments affect matching:
1172 #
1173 # - at_line_start True: sequence must start the line.
1174 # - at_whitespace_end True: sequence must be first non-whitespace text of the line.
1175 # - at_word_start True: sequence must start a word.
1176 # - hash_char The first character that must match in a regular expression.
1177 # - no_escape: True: ignore an 'end' string if it is preceded by
1178 # the ruleset's escape character.
1179 # - no_line_break True: the match will not succeed across line breaks.
1180 # - no_word_break: True: the match will not cross word breaks.
1181 #
1182 # The following arguments affect coloring when a match succeeds:
1183 #
1184 # - delegate A ruleset name. The matched text will be colored recursively
1185 # by the indicated ruleset.
1186 # - exclude_match If True, the actual text that matched will not be colored.
1187 # - kind The color tag to be applied to colored text.
1188 #@+node:ekr.20110605121601.18591: *4* jedit.dump
1189 def dump(self, s):
1190 if s.find('\n') == -1:
1191 return s
1192 return '\n' + s + '\n'
1193 #@+node:ekr.20110605121601.18592: *4* jedit.Leo rule functions
1194 #@+node:ekr.20110605121601.18593: *5* jedit.match_at_color
1195 def match_at_color(self, s, i):
1196 if self.trace_leo_matches:
1197 g.trace()
1198 # Only matches at start of line.
1199 if i == 0 and g.match_word(s, 0, '@color'):
1200 n = self.setRestart(self.restartColor)
1201 self.setState(n) # Enable coloring of *this* line.
1202 self.colorRangeWithTag(s, 0, len('@color'), 'leokeyword')
1203 # Now required. Sets state.
1204 return len('@color')
1205 return 0
1206 #@+node:ekr.20170125140113.1: *6* restartColor
1207 def restartColor(self, s):
1208 """Change all lines up to the next color directive."""
1209 if g.match_word(s, 0, '@killcolor'):
1210 self.colorRangeWithTag(s, 0, len('@color'), 'leokeyword')
1211 self.setRestart(self.restartKillColor)
1212 return -len(s) # Continue to suppress coloring.
1213 if g.match_word(s, 0, '@nocolor-node'):
1214 self.setRestart(self.restartNoColorNode)
1215 return -len(s) # Continue to suppress coloring.
1216 if g.match_word(s, 0, '@nocolor'):
1217 self.setRestart(self.restartNoColor)
1218 return -len(s) # Continue to suppress coloring.
1219 n = self.setRestart(self.restartColor)
1220 self.setState(n) # Enables coloring of *this* line.
1221 return 0 # Allow colorizing!
1222 #@+node:ekr.20110605121601.18597: *5* jedit.match_at_killcolor & restarter
1223 def match_at_killcolor(self, s, i):
1225 # Only matches at start of line.
1226 if i == 0 and g.match_word(s, i, '@killcolor'):
1227 self.setRestart(self.restartKillColor)
1228 return len(s) # Match everything.
1229 return 0
1230 #@+node:ekr.20110605121601.18598: *6* jedit.restartKillColor
1231 def restartKillColor(self, s):
1232 self.setRestart(self.restartKillColor)
1233 return len(s) + 1
1234 #@+node:ekr.20110605121601.18594: *5* jedit.match_at_language
1235 def match_at_language(self, s, i):
1236 """Match Leo's @language directive."""
1237 # Only matches at start of line.
1238 if i != 0:
1239 return 0
1240 if g.match_word(s, i, '@language'):
1241 old_name = self.language
1242 j = g.skip_ws(s, i + len('@language'))
1243 k = g.skip_c_id(s, j)
1244 name = s[j:k]
1245 ok = self.init_mode(name)
1246 if ok:
1247 self.colorRangeWithTag(s, i, k, 'leokeyword')
1248 if name != old_name:
1249 # Solves the recoloring problem!
1250 n = self.setInitialStateNumber()
1251 self.setState(n)
1252 return k - i
1253 return 0
1254 #@+node:ekr.20110605121601.18595: *5* jedit.match_at_nocolor & restarter
1255 def match_at_nocolor(self, s, i):
1257 if self.trace_leo_matches:
1258 g.trace(i, repr(s))
1259 # Only matches at start of line.
1260 if i == 0 and not g.match(s, i, '@nocolor-') and g.match_word(s, i, '@nocolor'):
1261 self.setRestart(self.restartNoColor)
1262 return len(s) # Match everything.
1263 return 0
1264 #@+node:ekr.20110605121601.18596: *6* jedit.restartNoColor
1265 def restartNoColor(self, s):
1266 if self.trace_leo_matches:
1267 g.trace(repr(s))
1268 if g.match_word(s, 0, '@color'):
1269 n = self.setRestart(self.restartColor)
1270 self.setState(n) # Enables coloring of *this* line.
1271 self.colorRangeWithTag(s, 0, len('@color'), 'leokeyword')
1272 return len('@color')
1273 self.setRestart(self.restartNoColor)
1274 return len(s) # Match everything.
1275 #@+node:ekr.20110605121601.18599: *5* jedit.match_at_nocolor_node & restarter
1276 def match_at_nocolor_node(self, s, i):
1278 # Only matches at start of line.
1279 if i == 0 and g.match_word(s, i, '@nocolor-node'):
1280 self.setRestart(self.restartNoColorNode)
1281 return len(s) # Match everything.
1282 return 0
1283 #@+node:ekr.20110605121601.18600: *6* jedit.restartNoColorNode
1284 def restartNoColorNode(self, s):
1285 self.setRestart(self.restartNoColorNode)
1286 return len(s) + 1
1287 #@+node:ekr.20150622072456.1: *5* jedit.match_at_wrap
1288 def match_at_wrap(self, s, i):
1289 """Match Leo's @wrap directive."""
1290 c = self.c
1291 # Only matches at start of line.
1292 seq = '@wrap'
1293 if i == 0 and g.match_word(s, i, seq):
1294 j = i + len(seq)
1295 k = g.skip_ws(s, j)
1296 self.colorRangeWithTag(s, i, k, 'leokeyword')
1297 c.frame.forceWrap(c.p)
1298 return k - i
1299 return 0
1300 #@+node:ekr.20110605121601.18601: *5* jedit.match_blanks
1301 def match_blanks(self, s, i):
1302 # Use Qt code to show invisibles.
1303 return 0
1304 #@+node:ekr.20110605121601.18602: *5* jedit.match_doc_part & restarter
1305 def match_doc_part(self, s, i):
1306 """
1307 Colorize Leo's @ and @ doc constructs.
1308 Matches only at the start of the line.
1309 """
1310 if i != 0:
1311 return 0
1312 if g.match_word(s, i, '@doc'):
1313 j = i + 4
1314 elif g.match(s, i, '@') and (i + 1 >= len(s) or s[i + 1] in (' ', '\t', '\n')):
1315 j = i + 1
1316 else:
1317 return 0
1318 c = self.c
1319 self.colorRangeWithTag(s, 0, j, 'leokeyword')
1320 # New in Leo 5.5: optionally colorize doc parts using reStructuredText
1321 if c.config.getBool('color-doc-parts-as-rest'):
1322 # Switch langauges.
1323 self.after_doc_language = self.language
1324 self.language = 'rest'
1325 self.clearState()
1326 self.init(c.p)
1327 # Restart.
1328 self.setRestart(self.restartDocPart)
1329 # Do *not* color the text here!
1330 return j
1331 self.clearState()
1332 self.setRestart(self.restartDocPart)
1333 self.colorRangeWithTag(s, j, len(s), 'docpart')
1334 return len(s)
1335 #@+node:ekr.20110605121601.18603: *6* jedit.restartDocPart
1336 def restartDocPart(self, s):
1337 """
1338 Restarter for @ and @ contructs.
1339 Continue until an @c, @code or @language at the start of the line.
1340 """
1341 for tag in ('@c', '@code', '@language'):
1342 if g.match_word(s, 0, tag):
1343 if tag == '@language':
1344 return self.match_at_language(s, 0)
1345 j = len(tag)
1346 self.colorRangeWithTag(s, 0, j, 'leokeyword') # 'docpart')
1347 # Switch languages.
1348 self.language = self.after_doc_language
1349 self.clearState()
1350 self.init(self.c.p)
1351 self.after_doc_language = None
1352 return j
1353 # Color the next line.
1354 self.setRestart(self.restartDocPart)
1355 if self.c.config.getBool('color-doc-parts-as-rest'):
1356 # Do *not* colorize the text here.
1357 return 0
1358 self.colorRangeWithTag(s, 0, len(s), 'docpart')
1359 return len(s)
1360 #@+node:ekr.20170204072452.1: *5* jedit.match_image
1361 image_url = re.compile(r'^\s*<\s*img\s+.*src=\"(.*)\".*>\s*$')
1363 def match_image(self, s, i):
1364 """Matcher for <img...>"""
1365 m = self.image_url.match(s, i)
1366 if m:
1367 self.image_src = src = m.group(1)
1368 j = len(src)
1369 doc = self.highlighter.document()
1370 block_n = self.currentBlockNumber()
1371 text_block = doc.findBlockByNumber(block_n)
1372 g.trace(f"block_n: {block_n:2} {s!r}")
1373 g.trace(f"block text: {repr(text_block.text())}")
1374 # How to get the cursor of the colorized line.
1375 # body = self.c.frame.body
1376 # s = body.wrapper.getAllText()
1377 # wrapper.delete(0, j)
1378 # cursor.insertHtml(src)
1379 return j
1380 return 0
1381 #@+node:ekr.20110605121601.18604: *5* jedit.match_leo_keywords
1382 def match_leo_keywords(self, s, i):
1383 """Succeed if s[i:] is a Leo keyword."""
1384 self.totalLeoKeywordsCalls += 1
1385 if s[i] != '@':
1386 return 0
1387 # fail if something besides whitespace precedes the word on the line.
1388 i2 = i - 1
1389 while i2 >= 0:
1390 ch = s[i2]
1391 if ch == '\n':
1392 break
1393 elif ch in (' ', '\t'):
1394 i2 -= 1
1395 else:
1396 return 0
1397 # Get the word as quickly as possible.
1398 j = i + 1
1399 while j < len(s) and s[j] in self.word_chars:
1400 j += 1
1401 word = s[i + 1 : j] # entries in leoKeywordsDict do not start with '@'.
1402 if j < len(s) and s[j] not in (' ', '\t', '\n'):
1403 return 0 # Fail, but allow a rescan, as in objective_c.
1404 if self.leoKeywordsDict.get(word):
1405 kind = 'leokeyword'
1406 self.colorRangeWithTag(s, i, j, kind)
1407 self.prev = (i, j, kind)
1408 result = j - i + 1 # Bug fix: skip the last character.
1409 self.trace_match(kind, s, i, j)
1410 return result
1411 # 2010/10/20: also check the keywords dict here.
1412 # This allows for objective_c keywords starting with '@'
1413 # This will not slow down Leo, because it is called
1414 # for things that look like Leo directives.
1415 word = '@' + word
1416 kind = self.keywordsDict.get(word)
1417 if kind:
1418 self.colorRangeWithTag(s, i, j, kind)
1419 self.prev = (i, j, kind)
1420 self.trace_match(kind, s, i, j)
1421 return j - i
1422 # Bug fix: allow rescan. Affects @language patch.
1423 return 0
1424 #@+node:ekr.20110605121601.18605: *5* jedit.match_section_ref
1425 def match_section_ref(self, s, i):
1426 p = self.c.p
1427 if self.trace_leo_matches:
1428 g.trace(self.section_delim1, self.section_delim2, s)
1429 #
1430 # Special case for @language patch: section references are not honored.
1431 if self.language == 'patch':
1432 return 0
1433 n1, n2 = len(self.section_delim1), len(self.section_delim2)
1434 if not g.match(s, i, self.section_delim1):
1435 return 0
1436 k = g.find_on_line(s, i + n1, self.section_delim2)
1437 if k == -1:
1438 return 0
1439 j = k + n2
1440 # Special case for @section-delims.
1441 if s.startswith('@section-delims'):
1442 self.colorRangeWithTag(s, i, i + n1, 'namebrackets')
1443 self.colorRangeWithTag(s, k, j, 'namebrackets')
1444 return j - i
1445 # An actual section reference.
1446 self.colorRangeWithTag(s, i, i + n1, 'namebrackets')
1447 ref = g.findReference(s[i:j], p)
1448 if ref:
1449 if self.use_hyperlinks:
1450 #@+<< set the hyperlink >>
1451 #@+node:ekr.20110605121601.18606: *6* << set the hyperlink >> (jedit)
1452 # Set the bindings to VNode callbacks.
1453 tagName = "hyper" + str(self.hyperCount)
1454 self.hyperCount += 1
1455 ref.tagName = tagName
1456 #@-<< set the hyperlink >>
1457 else:
1458 self.colorRangeWithTag(s, i + n1, k, 'link')
1459 else:
1460 self.colorRangeWithTag(s, i + n1, k, 'name')
1461 self.colorRangeWithTag(s, k, j, 'namebrackets')
1462 return j - i
1463 #@+node:ekr.20110605121601.18607: *5* jedit.match_tabs
1464 def match_tabs(self, s, i):
1465 # Use Qt code to show invisibles.
1466 return 0
1467 # Old code...
1468 # if not self.showInvisibles:
1469 # return 0
1470 # if self.trace_leo_matches: g.trace()
1471 # j = i; n = len(s)
1472 # while j < n and s[j] == '\t':
1473 # j += 1
1474 # if j > i:
1475 # self.colorRangeWithTag(s, i, j, 'tab')
1476 # return j - i
1477 # return 0
1478 #@+node:tbrown.20170707150713.1: *5* jedit.match_tabs
1479 def match_trailing_ws(self, s, i):
1480 """match trailing whitespace"""
1481 j = i
1482 n = len(s)
1483 while j < n and s[j] in ' \t':
1484 j += 1
1485 if j > i and j == n:
1486 self.colorRangeWithTag(s, i, j, 'trailing_whitespace')
1487 return j - i
1488 return 0
1489 #@+node:ekr.20170225103140.1: *5* jedit.match_unl
1490 def match_unl(self, s, i):
1491 if g.match(s.lower(), i, 'unl://'):
1492 j = len(s) # By default, color the whole line.
1493 # #2410: Limit the coloring if possible.
1494 if i > 0:
1495 ch = s[i-1]
1496 if ch in ('"', "'", '`'):
1497 k = s.find(ch, i)
1498 if k > -1:
1499 j = k
1500 self.colorRangeWithTag(s, i, j, 'url')
1501 return j
1502 return 0
1503 #@+node:ekr.20110605121601.18608: *5* jedit.match_url_any/f/h
1504 # Fix bug 893230: URL coloring does not work for many Internet protocols.
1505 # Added support for: gopher, mailto, news, nntp, prospero, telnet, wais
1507 url_regex_f = re.compile(r"""(file|ftp)://[^\s'"]+[\w=/]""")
1508 url_regex_g = re.compile(r"""gopher://[^\s'"]+[\w=/]""")
1509 url_regex_h = re.compile(r"""(http|https)://[^\s'"]+[\w=/]""")
1510 url_regex_m = re.compile(r"""mailto://[^\s'"]+[\w=/]""")
1511 url_regex_n = re.compile(r"""(news|nntp)://[^\s'"]+[\w=/]""")
1512 url_regex_p = re.compile(r"""prospero://[^\s'"]+[\w=/]""")
1513 url_regex_t = re.compile(r"""telnet://[^\s'"]+[\w=/]""")
1514 url_regex_w = re.compile(r"""wais://[^\s'"]+[\w=/]""")
1515 kinds = '(file|ftp|gopher|http|https|mailto|news|nntp|prospero|telnet|wais)'
1516 url_regex = re.compile(fr"""{kinds}://[^\s'"]+[\w=/]""")
1518 def match_any_url(self, s, i):
1519 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex)
1521 def match_url_f(self, s, i):
1522 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_f)
1524 def match_url_g(self, s, i):
1525 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_g)
1527 def match_url_h(self, s, i):
1528 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_h)
1530 def match_url_m(self, s, i):
1531 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_m)
1533 def match_url_n(self, s, i):
1534 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_n)
1536 def match_url_p(self, s, i):
1537 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_p)
1539 def match_url_t(self, s, i):
1540 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_t)
1542 def match_url_w(self, s, i):
1543 return self.match_compiled_regexp(s, i, kind='url', regexp=self.url_regex_w)
1544 #@+node:ekr.20110605121601.18609: *4* jedit.match_compiled_regexp
1545 def match_compiled_regexp(self, s, i, kind, regexp, delegate=''):
1546 """Succeed if the compiled regular expression regexp matches at s[i:]."""
1547 n = self.match_compiled_regexp_helper(s, i, regexp)
1548 if n > 0:
1549 j = i + n
1550 self.colorRangeWithTag(s, i, j, kind, delegate=delegate)
1551 self.prev = (i, j, kind)
1552 self.trace_match(kind, s, i, j)
1553 return n
1554 return 0
1555 #@+node:ekr.20110605121601.18610: *5* jedit.match_compiled_regexp_helper
1556 def match_compiled_regexp_helper(self, s, i, regex):
1557 """
1558 Return the length of the matching text if
1559 seq (a regular expression) matches the present position.
1560 """
1561 # Match succeeds or fails more quickly than search.
1562 self.match_obj = mo = regex.match(s, i) # re_obj.search(s,i)
1563 if mo is None:
1564 return 0
1565 start, end = mo.start(), mo.end()
1566 if start != i:
1567 return 0
1568 return end - start
1569 #@+node:ekr.20110605121601.18611: *4* jedit.match_eol_span
1570 def match_eol_span(self, s, i,
1571 kind=None, seq='',
1572 at_line_start=False, at_whitespace_end=False, at_word_start=False,
1573 delegate='', exclude_match=False
1574 ):
1575 """Succeed if seq matches s[i:]"""
1576 if at_line_start and i != 0 and s[i - 1] != '\n':
1577 return 0
1578 if at_whitespace_end and i != g.skip_ws(s, 0):
1579 return 0
1580 if at_word_start and i > 0 and s[i - 1] in self.word_chars:
1581 return 0
1582 if at_word_start and i + len(
1583 seq) + 1 < len(s) and s[i + len(seq)] in self.word_chars:
1584 return 0
1585 if g.match(s, i, seq):
1586 j = len(s)
1587 self.colorRangeWithTag(
1588 s, i, j, kind, delegate=delegate, exclude_match=exclude_match)
1589 self.prev = (i, j, kind)
1590 self.trace_match(kind, s, i, j)
1591 return j # (was j-1) With a delegate, this could clear state.
1592 return 0
1593 #@+node:ekr.20110605121601.18612: *4* jedit.match_eol_span_regexp
1594 def match_eol_span_regexp(self, s, i,
1595 kind='', regexp='',
1596 at_line_start=False, at_whitespace_end=False, at_word_start=False,
1597 delegate='', exclude_match=False
1598 ):
1599 """Succeed if the regular expression regex matches s[i:]."""
1600 if at_line_start and i != 0 and s[i - 1] != '\n':
1601 return 0
1602 if at_whitespace_end and i != g.skip_ws(s, 0):
1603 return 0
1604 if at_word_start and i > 0 and s[i - 1] in self.word_chars:
1605 return 0 # 7/5/2008
1606 n = self.match_regexp_helper(s, i, regexp)
1607 if n > 0:
1608 j = len(s)
1609 self.colorRangeWithTag(
1610 s, i, j, kind, delegate=delegate, exclude_match=exclude_match)
1611 self.prev = (i, j, kind)
1612 self.trace_match(kind, s, i, j)
1613 return j - i
1614 return 0
1615 #@+node:ekr.20110605121601.18613: *4* jedit.match_everything
1616 # def match_everything (self,s,i,kind=None,delegate='',exclude_match=False):
1617 # """Match the entire rest of the string."""
1618 # j = len(s)
1619 # self.colorRangeWithTag(s,i,j,kind,delegate=delegate)
1620 # return j
1621 #@+node:ekr.20110605121601.18614: *4* jedit.match_keywords
1622 # This is a time-critical method.
1624 def match_keywords(self, s, i):
1625 """
1626 Succeed if s[i:] is a keyword.
1627 Returning -len(word) for failure greatly reduces the number of times this
1628 method is called.
1629 """
1630 self.totalKeywordsCalls += 1
1631 # We must be at the start of a word.
1632 if i > 0 and s[i - 1] in self.word_chars:
1633 return 0
1634 # Get the word as quickly as possible.
1635 j = i
1636 n = len(s)
1637 chars = self.word_chars
1638 # Special cases...
1639 if self.language in ('haskell', 'clojure'):
1640 chars["'"] = "'"
1641 if self.language == 'c':
1642 chars['_'] = '_'
1643 while j < n and s[j] in chars:
1644 j += 1
1645 word = s[i:j]
1646 # Fix part of #585: A kludge for css.
1647 if self.language == 'css' and word.endswith(':'):
1648 j -= 1
1649 word = word[:-1]
1650 if not word:
1651 g.trace(
1652 'can not happen',
1653 repr(s[i : max(j, i + 1)]),
1654 repr(s[i : i + 10]),
1655 g.callers(),
1656 )
1657 return 0
1658 if self.ignore_case:
1659 word = word.lower()
1660 kind = self.keywordsDict.get(word)
1661 if kind:
1662 self.colorRangeWithTag(s, i, j, kind)
1663 self.prev = (i, j, kind)
1664 result = j - i
1665 self.trace_match(kind, s, i, j)
1666 return result
1667 return -len(word) # An important new optimization.
1668 #@+node:ekr.20110605121601.18615: *4* jedit.match_line
1669 def match_line(self, s, i, kind=None, delegate='', exclude_match=False):
1670 """Match the rest of the line."""
1671 j = g.skip_to_end_of_line(s, i)
1672 self.colorRangeWithTag(s, i, j, kind, delegate=delegate)
1673 return j - i
1674 #@+node:ekr.20190606201152.1: *4* jedit.match_lua_literal
1675 def match_lua_literal(self, s, i, kind):
1676 """Succeed if s[i:] is a lua literal. See #1175"""
1677 k = self.match_span(s, i, kind=kind, begin="[[", end="]]")
1678 if k not in (None, 0):
1679 return k
1680 if not g.match(s, i, '[='):
1681 return 0
1682 # Calculate begin and end, then just call match_span
1683 j = i + 2
1684 while g.match(s, j, '='):
1685 j += 1
1686 if not g.match(s, j, '['):
1687 return 0
1688 return self.match_span(s, i, kind=kind, begin=s[i:j], end=s[i + 1 : j] + ']')
1689 #@+node:ekr.20110605121601.18616: *4* jedit.match_mark_following & getNextToken
1690 def match_mark_following(self, s, i,
1691 kind='', pattern='',
1692 at_line_start=False, at_whitespace_end=False, at_word_start=False,
1693 exclude_match=False
1694 ):
1695 """Succeed if s[i:] matches pattern."""
1696 if not self.allow_mark_prev:
1697 return 0
1698 if at_line_start and i != 0 and s[i - 1] != '\n':
1699 return 0
1700 if at_whitespace_end and i != g.skip_ws(s, 0):
1701 return 0
1702 if at_word_start and i > 0 and s[i - 1] in self.word_chars:
1703 return 0 # 7/5/2008
1704 if (
1705 at_word_start
1706 and i + len(pattern) + 1 < len(s)
1707 and s[i + len(pattern)] in self.word_chars
1708 ):
1709 return 0
1710 if g.match(s, i, pattern):
1711 j = i + len(pattern)
1712 # self.colorRangeWithTag(s,i,j,kind,exclude_match=exclude_match)
1713 k = self.getNextToken(s, j)
1714 # 2011/05/31: Do not match *anything* unless there is a token following.
1715 if k > j:
1716 self.colorRangeWithTag(s, i, j, kind, exclude_match=exclude_match)
1717 self.colorRangeWithTag(s, j, k, kind, exclude_match=False)
1718 j = k
1719 self.prev = (i, j, kind)
1720 self.trace_match(kind, s, i, j)
1721 return j - i
1722 return 0
1723 #@+node:ekr.20110605121601.18617: *5* jedit.getNextToken
1724 def getNextToken(self, s, i):
1725 """
1726 Return the index of the end of the next token for match_mark_following.
1728 The jEdit docs are not clear about what a 'token' is, but experiments with jEdit
1729 show that token means a word, as defined by word_chars.
1730 """
1731 # 2011/05/31: Might we extend the concept of token?
1732 # If s[i] is not a word char, should we return just it?
1733 i0 = i
1734 while i < len(s) and s[i].isspace():
1735 i += 1
1736 i1 = i
1737 while i < len(s) and s[i] in self.word_chars:
1738 i += 1
1739 if i == i1:
1740 return i0
1741 return min(len(s), i)
1742 #@+node:ekr.20110605121601.18618: *4* jedit.match_mark_previous
1743 def match_mark_previous(self, s, i,
1744 kind='', pattern='',
1745 at_line_start=False, at_whitespace_end=False, at_word_start=False,
1746 exclude_match=False
1747 ):
1748 """
1749 Return the length of a matched SEQ or 0 if no match.
1751 'at_line_start': True: sequence must start the line.
1752 'at_whitespace_end':True: sequence must be first non-whitespace text of the line.
1753 'at_word_start': True: sequence must start a word.
1754 """
1755 # This match was causing most of the syntax-color problems.
1756 return 0 # 2009/6/23
1757 #@+node:ekr.20110605121601.18619: *4* jedit.match_regexp_helper
1758 def match_regexp_helper(self, s, i, pattern):
1759 """
1760 Return the length of the matching text if
1761 seq (a regular expression) matches the present position.
1762 """
1763 try:
1764 flags = re.MULTILINE
1765 if self.ignore_case:
1766 flags |= re.IGNORECASE
1767 re_obj = re.compile(pattern, flags)
1768 except Exception:
1769 # Do not call g.es here!
1770 g.trace(f"Invalid regular expression: {pattern}")
1771 return 0
1772 # Match succeeds or fails more quickly than search.
1773 self.match_obj = mo = re_obj.match(s, i) # re_obj.search(s,i)
1774 if mo is None:
1775 return 0
1776 start, end = mo.start(), mo.end()
1777 if start != i: # Bug fix 2007-12-18: no match at i
1778 return 0
1779 return end - start
1780 #@+node:ekr.20110605121601.18620: *4* jedit.match_seq
1781 def match_seq(self, s, i,
1782 kind='', seq='',
1783 at_line_start=False,
1784 at_whitespace_end=False,
1785 at_word_start=False,
1786 delegate=''
1787 ):
1788 """Succeed if s[:] mathces seq."""
1789 if at_line_start and i != 0 and s[i - 1] != '\n':
1790 j = i
1791 elif at_whitespace_end and i != g.skip_ws(s, 0):
1792 j = i
1793 elif at_word_start and i > 0 and s[i - 1] in self.word_chars: # 7/5/2008
1794 j = i
1795 if at_word_start and i + len(
1796 seq) + 1 < len(s) and s[i + len(seq)] in self.word_chars:
1797 j = i # 7/5/2008
1798 elif g.match(s, i, seq):
1799 j = i + len(seq)
1800 self.colorRangeWithTag(s, i, j, kind, delegate=delegate)
1801 self.prev = (i, j, kind)
1802 self.trace_match(kind, s, i, j)
1803 else:
1804 j = i
1805 return j - i
1806 #@+node:ekr.20110605121601.18621: *4* jedit.match_seq_regexp
1807 def match_seq_regexp(self, s, i,
1808 kind='', regexp='',
1809 at_line_start=False, at_whitespace_end=False, at_word_start=False,
1810 delegate=''
1811 ):
1812 """Succeed if the regular expression regexp matches at s[i:]."""
1813 if at_line_start and i != 0 and s[i - 1] != '\n':
1814 return 0
1815 if at_whitespace_end and i != g.skip_ws(s, 0):
1816 return 0
1817 if at_word_start and i > 0 and s[i - 1] in self.word_chars:
1818 return 0
1819 n = self.match_regexp_helper(s, i, regexp)
1820 j = i + n
1821 assert j - i == n
1822 self.colorRangeWithTag(s, i, j, kind, delegate=delegate)
1823 self.prev = (i, j, kind)
1824 self.trace_match(kind, s, i, j)
1825 return j - i
1826 #@+node:ekr.20110605121601.18622: *4* jedit.match_span & helper & restarter
1827 def match_span(self, s, i,
1828 kind='', begin='', end='',
1829 at_line_start=False, at_whitespace_end=False, at_word_start=False,
1830 delegate='', exclude_match=False,
1831 no_escape=False, no_line_break=False, no_word_break=False
1832 ):
1833 """Succeed if s[i:] starts with 'begin' and contains a following 'end'."""
1834 dots = False # A flag that we are using dots as a continuation.
1835 if i >= len(s):
1836 return 0
1837 if at_line_start and i != 0 and s[i - 1] != '\n':
1838 j = i
1839 elif at_whitespace_end and i != g.skip_ws(s, 0):
1840 j = i
1841 elif at_word_start and i > 0 and s[i - 1] in self.word_chars:
1842 j = i
1843 elif at_word_start and i + len(
1844 begin) + 1 < len(s) and s[i + len(begin)] in self.word_chars:
1845 j = i
1846 elif not g.match(s, i, begin):
1847 j = i
1848 else:
1849 # We have matched the start of the span.
1850 j = self.match_span_helper(s, i + len(begin), end,
1851 no_escape, no_line_break, no_word_break=no_word_break)
1852 if j == -1:
1853 j = i # A real failure.
1854 else:
1855 # A hack to handle continued strings. Should work for most languages.
1856 # Prepend "dots" to the kind, as a flag to setTag.
1857 dots = j > len(
1858 s) and begin in "'\"" and end in "'\"" and kind.startswith('literal')
1859 dots = dots and self.language not in ('lisp', 'elisp', 'rust')
1860 if dots:
1861 kind = 'dots' + kind
1862 # A match
1863 i2 = i + len(begin)
1864 j2 = j + len(end)
1865 if delegate:
1866 self.colorRangeWithTag(
1867 s, i, i2, kind, delegate=None, exclude_match=exclude_match)
1868 self.colorRangeWithTag(
1869 s, i2, j, kind, delegate=delegate, exclude_match=exclude_match)
1870 self.colorRangeWithTag(
1871 s, j, j2, kind, delegate=None, exclude_match=exclude_match)
1872 else:
1873 self.colorRangeWithTag(
1874 s, i, j2, kind, delegate=None, exclude_match=exclude_match)
1875 j = j2
1876 self.prev = (i, j, kind)
1877 self.trace_match(kind, s, i, j)
1878 # New in Leo 5.5: don't recolor everything after continued strings.
1879 if j > len(s) and not dots:
1880 j = len(s) + 1
1882 def span(s):
1883 # Note: bindings are frozen by this def.
1884 return self.restart_match_span(s,
1885 # Positional args, in alpha order
1886 delegate, end, exclude_match, kind,
1887 no_escape, no_line_break, no_word_break)
1889 self.setRestart(span,
1890 # These must be keyword args.
1891 delegate=delegate, end=end,
1892 exclude_match=exclude_match,
1893 kind=kind,
1894 no_escape=no_escape,
1895 no_line_break=no_line_break,
1896 no_word_break=no_word_break)
1897 return j - i # Correct, whatever j is.
1898 #@+node:ekr.20110605121601.18623: *5* jedit.match_span_helper
1899 def match_span_helper(self, s, i, pattern, no_escape, no_line_break, no_word_break):
1900 """
1901 Return n >= 0 if s[i] ends with a non-escaped 'end' string.
1902 """
1903 esc = self.escape
1904 # pylint: disable=inconsistent-return-statements
1905 while 1:
1906 j = s.find(pattern, i)
1907 if j == -1:
1908 # Match to end of text if not found and no_line_break is False
1909 if no_line_break:
1910 return -1
1911 return len(s) + 1
1912 if no_word_break and j > 0 and s[j - 1] in self.word_chars:
1913 return -1 # New in Leo 4.5.
1914 if no_line_break and '\n' in s[i:j]:
1915 return -1
1916 if esc and not no_escape:
1917 # Only an odd number of escapes is a 'real' escape.
1918 escapes = 0
1919 k = 1
1920 while j - k >= 0 and s[j - k] == esc:
1921 escapes += 1
1922 k += 1
1923 if (escapes % 2) == 1:
1924 assert s[j - 1] == esc
1925 i += 1 # 2013/08/26: just advance past the *one* escaped character.
1926 else:
1927 return j
1928 else:
1929 return j
1930 # For pylint.
1931 return -1
1932 #@+node:ekr.20110605121601.18624: *5* jedit.restart_match_span
1933 def restart_match_span(self, s,
1934 delegate, end, exclude_match, kind,
1935 no_escape, no_line_break, no_word_break
1936 ):
1937 """Remain in this state until 'end' is seen."""
1938 self.matcher_name = 'restart:' + self.matcher_name.replace('restart:', '')
1939 i = 0
1940 j = self.match_span_helper(s, i, end, no_escape, no_line_break, no_word_break)
1941 if j == -1:
1942 j2 = len(s) + 1
1943 elif j > len(s):
1944 j2 = j
1945 else:
1946 j2 = j + len(end)
1947 if delegate:
1948 self.colorRangeWithTag(s, i, j, kind,
1949 delegate=delegate, exclude_match=exclude_match)
1950 self.colorRangeWithTag(s, j, j2, kind,
1951 delegate=None, exclude_match=exclude_match)
1952 else: # avoid having to merge ranges in addTagsToList.
1953 self.colorRangeWithTag(s, i, j2, kind,
1954 delegate=None, exclude_match=exclude_match)
1955 j = j2
1956 self.trace_match(kind, s, i, j)
1957 if j > len(s):
1959 def span(s):
1960 return self.restart_match_span(s,
1961 # Positional args, in alpha order
1962 delegate, end, exclude_match, kind,
1963 no_escape, no_line_break, no_word_break)
1965 self.setRestart(span,
1966 # These must be keywords args.
1967 delegate=delegate, end=end, kind=kind,
1968 no_escape=no_escape,
1969 no_line_break=no_line_break,
1970 no_word_break=no_word_break)
1971 else:
1972 self.clearState()
1973 return j # Return the new i, *not* the length of the match.
1974 #@+node:ekr.20110605121601.18625: *4* jedit.match_span_regexp
1975 def match_span_regexp(self, s, i,
1976 kind='', begin='', end='',
1977 at_line_start=False, at_whitespace_end=False, at_word_start=False,
1978 delegate='', exclude_match=False,
1979 no_escape=False, no_line_break=False, no_word_break=False,
1980 ):
1981 """
1982 Succeed if s[i:] starts with 'begin' (a regular expression) and
1983 contains a following 'end'.
1984 """
1985 if at_line_start and i != 0 and s[i - 1] != '\n':
1986 return 0
1987 if at_whitespace_end and i != g.skip_ws(s, 0):
1988 return 0
1989 if at_word_start and i > 0 and s[i - 1] in self.word_chars:
1990 return 0 # 7/5/2008
1991 if (
1992 at_word_start
1993 and i + len(begin) + 1 < len(s)
1994 and s[i + len(begin)] in self.word_chars
1995 ):
1996 return 0 # 7/5/2008
1997 n = self.match_regexp_helper(s, i, begin)
1998 # We may have to allow $n here, in which case we must use a regex object?
1999 if n > 0:
2000 j = i + n
2001 j2 = s.find(end, j)
2002 if j2 == -1:
2003 return 0
2004 if self.escape and not no_escape:
2005 # Only an odd number of escapes is a 'real' escape.
2006 escapes = 0
2007 k = 1
2008 while j - k >= 0 and s[j - k] == self.escape:
2009 escapes += 1
2010 k += 1
2011 if (escapes % 2) == 1:
2012 # An escaped end **aborts the entire match**:
2013 # there is no way to 'restart' the regex.
2014 return 0
2015 i2 = j2 - len(end)
2016 if delegate:
2017 self.colorRangeWithTag(
2018 s, i, j, kind, delegate=None, exclude_match=exclude_match)
2019 self.colorRangeWithTag(
2020 s, j, i2, kind, delegate=delegate, exclude_match=False)
2021 self.colorRangeWithTag(
2022 s, i2, j2, kind, delegate=None, exclude_match=exclude_match)
2023 else: # avoid having to merge ranges in addTagsToList.
2024 self.colorRangeWithTag(
2025 s, i, j2, kind, delegate=None, exclude_match=exclude_match)
2026 self.prev = (i, j, kind)
2027 self.trace_match(kind, s, i, j2)
2028 return j2 - i
2029 return 0
2030 #@+node:ekr.20190623132338.1: *4* jedit.match_tex_backslash
2031 ascii_letters = re.compile(r'[a-zA-Z]+')
2033 def match_tex_backslash(self, s, i, kind):
2034 """
2035 Match the tex s[i:].
2037 (Conventional) acro names are a backslashe followed by either:
2038 1. One or more ascii letters, or
2039 2. Exactly one character, of any kind.
2040 """
2041 assert s[i] == '\\'
2042 m = self.ascii_letters.match(s, i + 1)
2043 if m:
2044 n = len(m.group(0))
2045 j = i + n + 1
2046 else:
2047 # Colorize the backslash plus exactly one more character.
2048 j = i + 2
2049 self.colorRangeWithTag(s, i, j, kind, delegate='')
2050 self.prev = (i, j, kind)
2051 self.trace_match(kind, s, i, j)
2052 return j - i
2053 #@+node:ekr.20170205074106.1: *4* jedit.match_wiki_pattern
2054 def match_wiki_pattern(self, s, i, pattern):
2055 """Show or hide a regex pattern managed by the wikiview plugin."""
2056 m = pattern.match(s, i)
2057 if m:
2058 n = len(m.group(0))
2059 self.colorRangeWithTag(s, i, i + n, 'url')
2060 return n
2061 return 0
2062 #@+node:ekr.20110605121601.18626: *4* jedit.match_word_and_regexp
2063 def match_word_and_regexp(self, s, i,
2064 kind1='', word='',
2065 kind2='', pattern='',
2066 at_line_start=False, at_whitespace_end=False, at_word_start=False,
2067 exclude_match=False
2068 ):
2069 """Succeed if s[i:] matches pattern."""
2070 if not self.allow_mark_prev:
2071 return 0
2072 if at_line_start and i != 0 and s[i - 1] != '\n':
2073 return 0
2074 if at_whitespace_end and i != g.skip_ws(s, 0):
2075 return 0
2076 if at_word_start and i > 0 and s[i - 1] in self.word_chars:
2077 return 0
2078 if (
2079 at_word_start
2080 and i + len(word) + 1 < len(s)
2081 and s[i + len(word)] in self.word_chars
2082 ):
2083 j = i
2084 if not g.match(s, i, word):
2085 return 0
2086 j = i + len(word)
2087 n = self.match_regexp_helper(s, j, pattern)
2088 if n == 0:
2089 return 0
2090 self.colorRangeWithTag(s, i, j, kind1, exclude_match=exclude_match)
2091 k = j + n
2092 self.colorRangeWithTag(s, j, k, kind2, exclude_match=False)
2093 self.prev = (j, k, kind2)
2094 self.trace_match(kind1, s, i, j)
2095 self.trace_match(kind2, s, j, k)
2096 return k - i
2097 #@+node:ekr.20110605121601.18627: *4* jedit.skip_line
2098 def skip_line(self, s, i):
2099 if self.escape:
2100 escape = self.escape + '\n'
2101 n = len(escape)
2102 while i < len(s):
2103 j = g.skip_line(s, i)
2104 if not g.match(s, j - n, escape):
2105 return j
2106 i = j
2107 return i
2108 return g.skip_line(s, i)
2109 # Include the newline so we don't get a flash at the end of the line.
2110 #@+node:ekr.20110605121601.18628: *4* jedit.trace_match
2111 def trace_match(self, kind, s, i, j):
2113 if j != i and self.trace_match_flag:
2114 g.trace(kind, i, j, g.callers(2), self.dump(s[i:j]))
2115 #@+node:ekr.20110605121601.18629: *3* jedit.State methods
2116 #@+node:ekr.20110605121601.18630: *4* jedit.clearState
2117 def clearState(self):
2118 """
2119 Create a *language-specific* default state.
2120 This properly forces a full recoloring when @language changes.
2121 """
2122 n = self.initialStateNumber
2123 self.setState(n)
2124 return n
2125 #@+node:ekr.20110605121601.18631: *4* jedit.computeState
2126 def computeState(self, f, keys):
2127 """
2128 Compute the state name associated with f and all the keys.
2129 Return a unique int n representing that state.
2130 """
2131 # Abbreviate arg names.
2132 d = {
2133 'delegate': '=>',
2134 'end': 'end',
2135 'at_line_start': 'start',
2136 'at_whitespace_end': 'ws-end',
2137 'exclude_match': '!match',
2138 'no_escape': '!esc',
2139 'no_line_break': '!lbrk',
2140 'no_word_break': '!wbrk',
2141 }
2142 result = [self.languageTag(self.language)]
2143 if not self.rulesetName.endswith('_main'):
2144 result.append(self.rulesetName)
2145 if f:
2146 result.append(f.__name__)
2147 for key in sorted(keys):
2148 keyVal = keys.get(key)
2149 val = d.get(key)
2150 if val is None:
2151 val = keys.get(key)
2152 result.append(f"{key}={val}")
2153 elif keyVal is True:
2154 result.append(f"{val}")
2155 elif keyVal is False:
2156 pass
2157 elif keyVal not in (None, ''):
2158 result.append(f"{key}={keyVal}")
2159 state = ';'.join(result).lower()
2160 table = (
2161 ('kind=', ''),
2162 ('literal', 'lit'),
2163 ('restart', '@'),
2164 )
2165 for pattern, s in table:
2166 state = state.replace(pattern, s)
2167 n = self.stateNameToStateNumber(f, state)
2168 return n
2169 #@+node:ekr.20110605121601.18632: *4* jedit.getters & setters
2170 def currentBlockNumber(self):
2171 block = self.highlighter.currentBlock()
2172 return block.blockNumber() if block and block.isValid() else -1
2174 def currentState(self):
2175 return self.highlighter.currentBlockState()
2177 def prevState(self):
2178 return self.highlighter.previousBlockState()
2180 def setState(self, n):
2181 self.highlighter.setCurrentBlockState(n)
2182 return n
2183 #@+node:ekr.20170125141148.1: *4* jedit.inColorState
2184 def inColorState(self):
2185 """True if the *current* state is enabled."""
2186 n = self.currentState()
2187 state = self.stateDict.get(n, 'no-state')
2188 enabled = (
2189 not state.endswith('@nocolor') and
2190 not state.endswith('@nocolor-node') and
2191 not state.endswith('@killcolor'))
2192 return enabled
2193 #@+node:ekr.20110605121601.18633: *4* jedit.setRestart
2194 def setRestart(self, f, **keys):
2195 n = self.computeState(f, keys)
2196 self.setState(n)
2197 return n
2198 #@+node:ekr.20110605121601.18635: *4* jedit.show...
2199 def showState(self, n):
2200 state = self.stateDict.get(n, 'no-state')
2201 return f"{n:2}:{state}"
2203 def showCurrentState(self):
2204 n = self.currentState()
2205 return self.showState(n)
2207 def showPrevState(self):
2208 n = self.prevState()
2209 return self.showState(n)
2210 #@+node:ekr.20110605121601.18636: *4* jedit.stateNameToStateNumber
2211 def stateNameToStateNumber(self, f, stateName):
2212 """
2213 stateDict: Keys are state numbers, values state names.
2214 stateNameDict: Keys are state names, values are state numbers.
2215 restartDict: Keys are state numbers, values are restart functions
2216 """
2217 n = self.stateNameDict.get(stateName)
2218 if n is None:
2219 n = self.nextState
2220 self.stateNameDict[stateName] = n
2221 self.stateDict[n] = stateName
2222 self.restartDict[n] = f
2223 self.nextState += 1
2224 self.n2languageDict[n] = self.language
2225 return n
2226 #@+node:ekr.20110605121601.18637: *3* jedit.colorRangeWithTag
2227 def colorRangeWithTag(self, s, i, j, tag, delegate='', exclude_match=False):
2228 """
2229 Actually colorize the selected range.
2231 This is called whenever a pattern matcher succeed.
2232 """
2233 trace = 'coloring' in g.app.debug and not g.unitTesting
2234 # setTag does most tracing.
2235 if not self.inColorState():
2236 # Do *not* check x.flag here. It won't work.
2237 if trace:
2238 g.trace('not in color state')
2239 return
2240 self.delegate_name = delegate
2241 if delegate:
2242 if trace:
2243 if len(repr(s[i:j])) <= 20:
2244 s2 = repr(s[i:j])
2245 else:
2246 s2 = repr(s[i : i + 17 - 2] + '...')
2247 kind_s = f"{delegate}:{tag}"
2248 print(
2249 f"\ncolorRangeWithTag: {kind_s:25} {i:3} {j:3} "
2250 f"{s2:>20} {self.matcher_name}\n")
2251 self.modeStack.append(self.modeBunch)
2252 self.init_mode(delegate)
2253 while 0 <= i < j and i < len(s):
2254 progress = i
2255 assert j >= 0, j
2256 for f in self.rulesDict.get(s[i], []):
2257 n = f(self, s, i)
2258 if n is None:
2259 g.trace('Can not happen: delegate matcher returns None')
2260 elif n > 0:
2261 self.matcher_name = f.__name__
2262 i += n
2263 break
2264 else:
2265 # Use the default chars for everything else.
2266 # Use the *delegate's* default characters if possible.
2267 default_tag = self.attributesDict.get('default')
2268 self.setTag(default_tag or tag, s, i, i + 1)
2269 i += 1
2270 assert i > progress
2271 bunch = self.modeStack.pop()
2272 self.initModeFromBunch(bunch)
2273 elif not exclude_match:
2274 self.setTag(tag, s, i, j)
2275 if tag != 'url':
2276 # Allow UNL's and URL's *everywhere*.
2277 j = min(j, len(s))
2278 while i < j:
2279 ch = s[i].lower()
2280 if ch == 'u':
2281 n = self.match_unl(s, i)
2282 i += max(1, n)
2283 elif ch in 'fh': # file|ftp|http|https
2284 n = self.match_any_url(s, i)
2285 i += max(1, n)
2286 else:
2287 i += 1
2288 #@+node:ekr.20110605121601.18638: *3* jedit.mainLoop
2289 tot_time = 0.0
2291 def mainLoop(self, n, s):
2292 """Colorize a *single* line s, starting in state n."""
2293 trace = 'coloring' in g.app.debug
2294 t1 = time.process_time()
2295 f = self.restartDict.get(n)
2296 if trace:
2297 p = self.c and self.c.p
2298 if p and p.v != self.last_v:
2299 self.last_v = p.v
2300 f_name = f.__name__ if f else 'None'
2301 print('')
2302 g.trace(f"NEW NODE: state {n} = {f_name} {p.h}\n")
2303 i = f(s) if f else 0
2304 while i < len(s):
2305 progress = i
2306 functions = self.rulesDict.get(s[i], [])
2307 for f in functions:
2308 n = f(self, s, i)
2309 if n is None:
2310 g.trace('Can not happen: n is None', repr(f))
2311 break
2312 elif n > 0: # Success. The match has already been colored.
2313 self.matcher_name = f.__name__ # For traces.
2314 i += n
2315 break
2316 elif n < 0: # Total failure.
2317 i += -n
2318 break
2319 else: # Partial failure: Do not break or change i!
2320 pass
2321 else:
2322 i += 1
2323 assert i > progress
2324 # Don't even *think* about changing state here.
2325 self.tot_time += time.process_time() - t1
2326 #@+node:ekr.20110605121601.18640: *3* jedit.recolor & helpers
2327 def recolor(self, s):
2328 """
2329 jEdit.recolor: Recolor a *single* line, s.
2330 QSyntaxHighligher calls this method repeatedly and automatically.
2331 """
2332 p = self.c.p
2333 self.recolorCount += 1
2334 block_n = self.currentBlockNumber()
2335 n = self.prevState()
2336 if p.v == self.old_v:
2337 new_language = self.n2languageDict.get(n)
2338 if new_language != self.language:
2339 self.language = new_language
2340 self.init(p)
2341 else:
2342 self.updateSyntaxColorer(p) # Force a full recolor
2343 assert self.language
2344 self.init_all_state(p.v)
2345 self.init(p)
2346 if block_n == 0:
2347 n = self.initBlock0()
2348 n = self.setState(n) # Required.
2349 # Always color the line, even if colorizing is disabled.
2350 if s:
2351 self.mainLoop(n, s)
2352 #@+node:ekr.20170126100139.1: *4* jedit.initBlock0
2353 def initBlock0(self):
2354 """
2355 Init *local* ivars when handling block 0.
2356 This prevents endless recalculation of the proper default state.
2357 """
2358 if self.enabled:
2359 n = self.setInitialStateNumber()
2360 else:
2361 n = self.setRestart(self.restartNoColor)
2362 return n
2363 #@+node:ekr.20170126101049.1: *4* jedit.setInitialStateNumber
2364 def setInitialStateNumber(self):
2365 """
2366 Init the initialStateNumber ivar for clearState()
2367 This saves a lot of work.
2369 Called from init() and initBlock0.
2370 """
2371 state = self.languageTag(self.language)
2372 n = self.stateNameToStateNumber(None, state)
2373 self.initialStateNumber = n
2374 self.blankStateNumber = self.stateNameToStateNumber(None, state + ';blank')
2375 return n
2376 #@+node:ekr.20170126103925.1: *4* jedit.languageTag
2377 def languageTag(self, name):
2378 """
2379 Return the standardized form of the language name.
2380 Doing this consistently prevents subtle bugs.
2381 """
2382 if name:
2383 table = (
2384 ('markdown', 'md'),
2385 ('python', 'py'),
2386 ('javascript', 'js'),
2387 )
2388 for pattern, s in table:
2389 name = name.replace(pattern, s)
2390 return name
2391 return 'no-language'
2392 #@+node:ekr.20170205055743.1: *3* jedit.set_wikiview_patterns
2393 def set_wikiview_patterns(self, leadins, patterns):
2394 """
2395 Init the colorizer so it will *skip* all patterns.
2396 The wikiview plugin calls this method.
2397 """
2398 d = self.rulesDict
2399 for leadins_list, pattern in zip(leadins, patterns):
2400 for ch in leadins_list:
2402 def wiki_rule(self, s, i, pattern=pattern):
2403 """Bind pattern and leadin for jedit.match_wiki_pattern."""
2404 return self.match_wiki_pattern(s, i, pattern)
2406 aList = d.get(ch, [])
2407 if wiki_rule not in aList:
2408 aList.insert(0, wiki_rule)
2409 d[ch] = aList
2410 self.rulesDict = d
2411 #@-others
2412#@+node:ekr.20110605121601.18565: ** class LeoHighlighter (QSyntaxHighlighter)
2413# Careful: we may be running from the bridge.
2415if QtGui:
2418 class LeoHighlighter(QtGui.QSyntaxHighlighter): # type:ignore
2419 """
2420 A subclass of QSyntaxHighlighter that overrides
2421 the highlightBlock and rehighlight methods.
2423 All actual syntax coloring is done in the highlighter class.
2425 Used by both the JeditColorizer and PYgmentsColorizer classes.
2426 """
2427 # This is c.frame.body.colorizer.highlighter
2428 #@+others
2429 #@+node:ekr.20110605121601.18566: *3* leo_h.ctor (sets style)
2430 def __init__(self, c, colorizer, document):
2431 """ctor for LeoHighlighter class."""
2432 self.c = c
2433 self.colorizer = colorizer
2434 self.n_calls = 0
2435 assert isinstance(document, QtGui.QTextDocument), document
2436 # Alas, a QsciDocument is not a QTextDocument.
2437 self.leo_document = document
2438 super().__init__(document)
2439 self.reloadSettings()
2440 #@+node:ekr.20110605121601.18567: *3* leo_h.highlightBlock
2441 def highlightBlock(self, s):
2442 """ Called by QSyntaxHighlighter """
2443 self.n_calls += 1
2444 s = g.toUnicode(s)
2445 self.colorizer.recolor(s)
2446 # Highlight just one line.
2447 #@+node:ekr.20190327052228.1: *3* leo_h.reloadSettings
2448 def reloadSettings(self):
2449 """Reload all reloadable settings."""
2450 c, document = self.c, self.leo_document
2451 if not pygments:
2452 return
2453 if not c.config.getBool('use-pygments', default=False):
2454 return
2455 #
2456 # Init pygments ivars.
2457 self._brushes = {}
2458 self._document = document
2459 self._formats = {}
2460 self.colorizer.style_name = 'default'
2461 style_name = c.config.getString('pygments-style-name') or 'default'
2462 # Style gallery: https://help.farbox.com/pygments.html
2463 # Dark styles: fruity, monokai, native, vim
2464 # https://github.com/gthank/solarized-dark-pygments
2465 if not c.config.getBool('use-pygments-styles', default=True):
2466 return
2467 #
2468 # Init pygments style.
2469 try:
2470 self.setStyle(style_name)
2471 # print('using %r pygments style in %r' % (style_name, c.shortFileName()))
2472 except Exception:
2473 print(f'pygments {style_name!r} style not found. Using "default" style')
2474 self.setStyle('default')
2475 style_name = 'default'
2476 self.colorizer.style_name = style_name
2477 assert self._style
2478 #@+node:ekr.20190320154014.1: *3* leo_h: From PygmentsHighlighter
2479 #
2480 # All code in this tree is based on PygmentsHighlighter.
2481 #
2482 # Copyright (c) Jupyter Development Team.
2483 # Distributed under the terms of the Modified BSD License.
2484 #@+others
2485 #@+node:ekr.20190320153605.1: *4* leo_h._get_format & helpers
2486 def _get_format(self, token):
2487 """ Returns a QTextCharFormat for token or None.
2488 """
2489 if token in self._formats:
2490 return self._formats[token]
2491 if self._style is None:
2492 result = self._get_format_from_document(token, self._document)
2493 else:
2494 result = self._get_format_from_style(token, self._style)
2495 result = self._get_format_from_style(token, self._style)
2496 self._formats[token] = result
2497 return result
2498 #@+node:ekr.20190320162831.1: *5* pyg_h._get_format_from_document
2499 def _get_format_from_document(self, token, document):
2500 """ Returns a QTextCharFormat for token by
2501 """
2502 # Modified by EKR.
2503 # These lines cause unbounded recursion.
2504 # code, html = next(self._formatter._format_lines([(token, u'dummy')]))
2505 # self._document.setHtml(html)
2506 return QtGui.QTextCursor(self._document).charFormat()
2507 #@+node:ekr.20190320153716.1: *5* leo_h._get_format_from_style
2508 key_error_d: Dict[str, bool] = {}
2510 def _get_format_from_style(self, token, style):
2511 """ Returns a QTextCharFormat for token by reading a Pygments style.
2512 """
2513 result = QtGui.QTextCharFormat()
2514 #
2515 # EKR: handle missing tokens.
2516 try:
2517 data = style.style_for_token(token).items()
2518 except KeyError as err:
2519 key = repr(err)
2520 if key not in self.key_error_d:
2521 self.key_error_d[key] = True
2522 g.trace(err)
2523 return result
2524 for key, value in data:
2525 if value:
2526 if key == 'color':
2527 result.setForeground(self._get_brush(value))
2528 elif key == 'bgcolor':
2529 result.setBackground(self._get_brush(value))
2530 elif key == 'bold':
2531 result.setFontWeight(Weight.Bold)
2532 elif key == 'italic':
2533 result.setFontItalic(True)
2534 elif key == 'underline':
2535 result.setUnderlineStyle(UnderlineStyle.SingleUnderline)
2536 elif key == 'sans':
2537 result.setFontStyleHint(Weight.SansSerif)
2538 elif key == 'roman':
2539 result.setFontStyleHint(Weight.Times)
2540 elif key == 'mono':
2541 result.setFontStyleHint(Weight.TypeWriter)
2542 return result
2543 #@+node:ekr.20190320153958.1: *4* leo_h.setStyle
2544 def setStyle(self, style):
2545 """ Sets the style to the specified Pygments style.
2546 """
2547 from pygments.styles import get_style_by_name # type:ignore
2549 if isinstance(style, str):
2550 style = get_style_by_name(style)
2551 self._style = style
2552 self._clear_caches()
2553 #@+node:ekr.20190320154604.1: *4* leo_h.clear_caches
2554 def _clear_caches(self):
2555 """ Clear caches for brushes and formats.
2556 """
2557 self._brushes = {}
2558 self._formats = {}
2559 #@+node:ekr.20190320154752.1: *4* leo_h._get_brush/color
2560 def _get_brush(self, color):
2561 """ Returns a brush for the color.
2562 """
2563 result = self._brushes.get(color)
2564 if result is None:
2565 qcolor = self._get_color(color)
2566 result = QtGui.QBrush(qcolor)
2567 self._brushes[color] = result
2568 return result
2570 def _get_color(self, color):
2571 """ Returns a QColor built from a Pygments color string.
2572 """
2573 qcolor = QtGui.QColor()
2574 qcolor.setRgb(int(color[:2], base=16),
2575 int(color[2:4], base=16),
2576 int(color[4:6], base=16))
2577 return qcolor
2578 #@-others
2579 #@-others
2580#@+node:ekr.20140906095826.18717: ** class NullScintillaLexer (QsciLexerCustom)
2581if Qsci:
2584 class NullScintillaLexer(Qsci.QsciLexerCustom): # type:ignore
2585 """A do-nothing colorizer for Scintilla."""
2587 def __init__(self, c, parent=None):
2588 super().__init__(parent)
2589 # Init the pase class
2590 self.leo_c = c
2591 self.configure_lexer()
2593 def description(self, style):
2594 return 'NullScintillaLexer'
2596 def setStyling(self, length, style):
2597 g.trace('(NullScintillaLexer)', length, style)
2599 def styleText(self, start, end):
2600 """Style the text from start to end."""
2602 def configure_lexer(self):
2603 """Configure the QScintilla lexer."""
2604 # c = self.leo_c
2605 lexer = self
2606 # To do: use c.config setting.
2607 # pylint: disable=no-member
2608 font = QtGui.QFont("DejaVu Sans Mono", 14)
2609 lexer.setFont(font)
2610#@+node:ekr.20190319151826.1: ** class PygmentsColorizer(BaseJEditColorizer)
2611class PygmentsColorizer(BaseJEditColorizer):
2612 """
2613 This class adapts pygments tokens to QSyntaxHighlighter.
2614 """
2615 # This is c.frame.body.colorizer
2616 #@+others
2617 #@+node:ekr.20190319151826.3: *3* pyg_c.__init__ & helpers
2618 def __init__(self, c, widget, wrapper):
2619 """Ctor for JEditColorizer class."""
2620 super().__init__(c, widget, wrapper)
2621 #
2622 # Create the highlighter. The default is NullObject.
2623 if isinstance(widget, QtWidgets.QTextEdit):
2624 self.highlighter = LeoHighlighter(c,
2625 colorizer=self,
2626 document=widget.document(),
2627 )
2628 #
2629 # State unique to this class...
2630 self.color_enabled = self.enabled
2631 self.old_v = None
2632 #
2633 # Init common data...
2634 # self.init_style_ivars()
2635 # self.defineLeoKeywordsDict()
2636 # self.defineDefaultColorsDict()
2637 # self.defineDefaultFontDict()
2638 self.reloadSettings()
2639 # self.init()
2640 #@+node:ekr.20190324043722.1: *4* pyg_c.init
2641 def init(self, p=None):
2642 """Init the colorizer. p is for tracing only."""
2643 #
2644 # Like jedit.init, but no need to init state.
2645 self.init_mode(self.language)
2646 self.prev = None
2647 # Used by setTag.
2648 self.configure_tags()
2650 def addLeoRules(self, theDict):
2651 pass
2652 #@+node:ekr.20190324051704.1: *4* pyg_c.reloadSettings
2653 def reloadSettings(self):
2654 """Reload the base settings, plus pygments settings."""
2655 if 'coloring' in g.app.debug and not g.unitTesting:
2656 print('reloading pygments settings.')
2657 # Do basic inits.
2658 BaseJEditColorizer.reloadSettings(self)
2659 # Bind methods.
2660 if self.use_pygments_styles:
2661 self.getDefaultFormat = QtGui.QTextCharFormat
2662 self.getFormat = self.getPygmentsFormat
2663 self.setFormat = self.setPygmentsFormat
2664 else:
2665 self.getDefaultFormat = self.getLegacyDefaultFormat
2666 self.getFormat = self.getLegacyFormat
2667 self.setFormat = self.setLegacyFormat
2668 # Init everything else.
2669 self.init_style_ivars()
2670 self.defineLeoKeywordsDict()
2671 self.defineDefaultColorsDict()
2672 self.defineDefaultFontDict()
2673 self.init()
2674 #@+node:ekr.20190324063349.1: *3* pyg_c.format getters
2675 def getLegacyDefaultFormat(self):
2676 return None
2678 traced_dict: Dict[str, str] = {}
2680 def getLegacyFormat(self, token, text):
2681 """Return a jEdit tag for the given pygments token."""
2682 r = repr(token).lstrip('Token.').lstrip('Literal.').lower()
2683 # Tables and setTag assume lower-case.
2684 if r == 'name':
2685 # Avoid a colision with existing Leo tag.
2686 r = 'name.pygments'
2687 if 0:
2688 if r not in self.traced_dict:
2689 self.traced_dict[r] = r
2690 g.trace(r)
2691 return r
2693 def getPygmentsFormat(self, token, text):
2694 """Return a pygments format."""
2695 format = self.highlighter._formats.get(token)
2696 if not format:
2697 format = self.highlighter._get_format(token)
2698 return format
2699 #@+node:ekr.20190324064341.1: *3* pyg_c.format setters
2700 def setLegacyFormat(self, index, length, format, s):
2701 """Call the jEdit style setTag."""
2702 BaseJEditColorizer.setTag(self, format, s, index, index + length)
2704 def setPygmentsFormat(self, index, length, format, s):
2705 """Call the base setTag to set the Qt format."""
2706 self.highlighter.setFormat(index, length, format)
2707 #@+node:ekr.20190319151826.78: *3* pyg_c.mainLoop & helpers
2708 format_dict: Dict[str, str] = {} # Keys are repr(Token), values are formats.
2709 lexers_dict: Dict[str, Callable] = {} # Keys are language names, values are instantiated, patched lexers.
2710 state_s_dict: Dict[str, int] = {} # Keys are strings, values are ints.
2711 state_n_dict: Dict[int, str] = {} # # Keys are ints, values are strings.
2712 state_index = 1 # Index of state number to be allocated.
2713 tot_time = 0.0
2715 def mainLoop(self, s):
2716 """Colorize a *single* line s"""
2717 t1 = time.process_time()
2718 highlighter = self.highlighter
2719 #
2720 # First, set the *expected* lexer. It may change later.
2721 lexer = self.set_lexer()
2722 #
2723 # Restore the state.
2724 # Based on Jupyter code: (c) Jupyter Development Team.
2725 stack_ivar = '_saved_state_stack'
2726 prev_data = highlighter.currentBlock().previous().userData()
2727 if prev_data is not None:
2728 # New code by EKR. Restore the language if necessary.
2729 if self.language != prev_data.leo_language:
2730 # Change the language and the lexer!
2731 self.language = prev_data.leo_language
2732 # g.trace('RESTORE:', self.language)
2733 lexer = self.set_lexer()
2734 setattr(lexer, stack_ivar, prev_data.syntax_stack)
2735 elif hasattr(lexer, stack_ivar):
2736 delattr(lexer, stack_ivar)
2737 # g.trace(self.color_enabled, self.language, repr(s))
2738 #
2739 # The main loop. Warning: this can change self.language.
2740 index = 0
2741 for token, text in lexer.get_tokens(s):
2742 length = len(text)
2743 # print('%5s %25r %r' % (self.color_enabled, repr(token).lstrip('Token.'), text))
2744 if self.color_enabled:
2745 format = self.getFormat(token, text)
2746 else:
2747 format = self.getDefaultFormat()
2748 self.setFormat(index, length, format, s)
2749 index += length
2750 #
2751 # Save the state.
2752 # Based on Jupyter code: (c) Jupyter Development Team.
2753 stack = getattr(lexer, stack_ivar, None)
2754 if stack:
2755 data = PygmentsBlockUserData(syntax_stack=stack, leo_language=self.language)
2756 highlighter.currentBlock().setUserData(data)
2757 # Clean up for the next go-round.
2758 delattr(lexer, stack_ivar)
2759 #
2760 # New code by EKR:
2761 # - Fixes a bug so multiline tokens work.
2762 # - State supports Leo's color directives.
2763 state_s = f"{self.language}; {self.color_enabled}: {stack!r}"
2764 state_n = self.state_s_dict.get(state_s)
2765 if state_n is None:
2766 state_n = self.state_index
2767 self.state_index += 1
2768 self.state_s_dict[state_s] = state_n
2769 self.state_n_dict[state_n] = state_s
2770 highlighter.setCurrentBlockState(state_n)
2771 self.tot_time += time.process_time() - t1
2772 #@+node:ekr.20190323045655.1: *4* pyg_c.at_color_callback
2773 def at_color_callback(self, lexer, match):
2774 from pygments.token import Name, Text # type: ignore
2775 kind = match.group(0)
2776 self.color_enabled = kind == '@color'
2777 if self.color_enabled:
2778 yield match.start(), Name.Decorator, kind
2779 else:
2780 yield match.start(), Text, kind
2781 #@+node:ekr.20190323045735.1: *4* pyg_c.at_language_callback
2782 def at_language_callback(self, lexer, match):
2783 from pygments.token import Name
2784 language = match.group(2)
2785 ok = self.init_mode(language)
2786 if ok:
2787 self.language = language
2788 yield match.start(), Name.Decorator, match.group(0)
2789 else:
2790 yield match.start(), Name.Decorator, match.group(1)
2791 # Color only the @language, indicating an unknown language.
2792 #@+node:ekr.20190322082533.1: *4* pyg_c.get_lexer
2793 def get_lexer(self, language):
2794 """Return the lexer for self.language, creating it if necessary."""
2795 import pygments.lexers as lexers # type: ignore
2796 tag = 'get_lexer'
2797 trace = 'coloring' in g.app.debug
2798 try:
2799 # #1520: always define lexer_language.
2800 lexer_name = 'python3' if language == 'python' else language
2801 lexer = lexers.get_lexer_by_name(lexer_name)
2802 except Exception:
2803 # pylint: disable=no-member
2804 # One of the lexer's will not exist.
2805 if trace:
2806 g.trace(f"{tag}: no lexer for {language!r}")
2807 lexer = lexers.Python3Lexer()
2808 if trace and 'python' not in self.lexers_dict:
2809 g.trace(f"{tag}: default lexer for python: {lexer!r}")
2810 return lexer
2811 #@+node:ekr.20190322094034.1: *4* pyg_c.patch_lexer
2812 def patch_lexer(self, language, lexer):
2814 from pygments.token import Comment # type:ignore
2815 from pygments.lexer import inherit # type:ignore
2818 class PatchedLexer(lexer.__class__): # type:ignore
2820 leo_sec_ref_pat = r'(?-m:\<\<(.*?)\>\>)'
2821 tokens = {
2822 'root': [
2823 (r'^@(color|nocolor|killcolor)\b', self.at_color_callback),
2824 (r'^(@language)\s+(\w+)', self.at_language_callback),
2825 (leo_sec_ref_pat, self.section_ref_callback),
2826 # Single-line, non-greedy match.
2827 (r'(^\s*@doc|@)(\s+|\n)(.|\n)*?^@c', Comment.Leo.DocPart),
2828 # Multi-line, non-greedy match.
2829 inherit,
2830 ],
2831 }
2833 try:
2834 return PatchedLexer()
2835 except Exception:
2836 g.trace(f"can not patch {language!r}")
2837 g.es_exception()
2838 return lexer
2839 #@+node:ekr.20190322133358.1: *4* pyg_c.section_ref_callback
2840 def section_ref_callback(self, lexer, match):
2841 """pygments callback for section references."""
2842 c = self.c
2843 from pygments.token import Comment, Name
2844 name, ref, start = match.group(1), match.group(0), match.start()
2845 found = g.findReference(ref, c.p)
2846 found_tok = Name.Entity if found else Name.Other
2847 yield match.start(), Comment, '<<'
2848 yield start + 2, found_tok, name
2849 yield start + 2 + len(name), Comment, '>>'
2850 #@+node:ekr.20190323064820.1: *4* pyg_c.set_lexer
2851 def set_lexer(self):
2852 """Return the lexer for self.language."""
2853 if self.language == 'patch':
2854 self.language = 'diff'
2855 key = f"{self.language}:{id(self)}"
2856 lexer = self.lexers_dict.get(key)
2857 if not lexer:
2858 lexer = self.get_lexer(self.language)
2859 lexer = self.patch_lexer(self.language, lexer)
2860 self.lexers_dict[key] = lexer
2861 return lexer
2862 #@+node:ekr.20190319151826.79: *3* pyg_c.recolor
2863 def recolor(self, s):
2864 """
2865 PygmentsColorizer.recolor: Recolor a *single* line, s.
2866 QSyntaxHighligher calls this method repeatedly and automatically.
2867 """
2868 p = self.c.p
2869 self.recolorCount += 1
2870 if p.v != self.old_v:
2871 self.updateSyntaxColorer(p)
2872 # Force a full recolor
2873 # sets self.language and self.enabled.
2874 self.color_enabled = self.enabled
2875 self.old_v = p.v
2876 # Fix a major performance bug.
2877 self.init(p)
2878 # Support
2879 assert self.language
2880 if s is not None:
2881 # For pygments, we *must* call for all lines.
2882 self.mainLoop(s)
2883 #@-others
2884#@+node:ekr.20140906081909.18689: ** class QScintillaColorizer(BaseColorizer)
2885# This is c.frame.body.colorizer
2888class QScintillaColorizer(BaseColorizer):
2889 """A colorizer for a QsciScintilla widget."""
2890 #@+others
2891 #@+node:ekr.20140906081909.18709: *3* qsc.__init__ & reloadSettings
2892 def __init__(self, c, widget, wrapper):
2893 """Ctor for QScintillaColorizer. widget is a """
2894 super().__init__(c)
2895 self.count = 0 # For unit testing.
2896 self.colorCacheFlag = False
2897 self.error = False # Set if there is an error in jeditColorizer.recolor
2898 self.flag = True # Per-node enable/disable flag.
2899 self.full_recolor_count = 0 # For unit testing.
2900 self.language = 'python' # set by scanLanguageDirectives.
2901 self.highlighter = None
2902 self.lexer = None # Set in changeLexer.
2903 widget.leo_colorizer = self
2904 # Define/configure various lexers.
2905 self.reloadSettings()
2906 if Qsci:
2907 self.lexersDict = self.makeLexersDict()
2908 self.nullLexer = NullScintillaLexer(c)
2909 else:
2910 self.lexersDict = {} # type:ignore
2911 self.nullLexer = g.NullObject() # type:ignore
2913 def reloadSettings(self):
2914 c = self.c
2915 self.enabled = c.config.getBool('use-syntax-coloring')
2916 #@+node:ekr.20170128141158.1: *3* qsc.scanColorDirectives (over-ride)
2917 def scanColorDirectives(self, p):
2918 """
2919 Return language based on the directives in p's ancestors.
2920 Same as BaseColorizer.scanColorDirectives, except it also scans p.b.
2921 """
2922 c = self.c
2923 root = p.copy()
2924 for p in root.self_and_parents(copy=False):
2925 language = g.findFirstValidAtLanguageDirective(p.b)
2926 if language:
2927 return language
2928 # Get the language from the nearest ancestor @<file> node.
2929 language = g.getLanguageFromAncestorAtFileNode(root) or c.target_language
2930 return language
2931 #@+node:ekr.20140906081909.18718: *3* qsc.changeLexer
2932 def changeLexer(self, language):
2933 """Set the lexer for the given language."""
2934 c = self.c
2935 wrapper = c.frame.body.wrapper
2936 w = wrapper.widget # A Qsci.QsciSintilla object.
2937 self.lexer = self.lexersDict.get(language, self.nullLexer) # type:ignore
2938 w.setLexer(self.lexer)
2939 #@+node:ekr.20140906081909.18707: *3* qsc.colorize
2940 def colorize(self, p):
2941 """The main Scintilla colorizer entry point."""
2942 # It would be much better to use QSyntaxHighlighter.
2943 # Alas, a QSciDocument is not a QTextDocument.
2944 self.updateSyntaxColorer(p)
2945 self.changeLexer(self.language)
2946 # if self.NEW:
2947 # # Works, but QScintillaWrapper.tag_configuration is presently a do-nothing.
2948 # for s in g.splitLines(p.b):
2949 # self.jeditColorizer.recolor(s)
2950 #@+node:ekr.20140906095826.18721: *3* qsc.configure_lexer
2951 def configure_lexer(self, lexer):
2952 """Configure the QScintilla lexer using @data qt-scintilla-styles."""
2953 c = self.c
2954 qcolor, qfont = QtGui.QColor, QtGui.QFont
2955 font = qfont("DejaVu Sans Mono", 14)
2956 lexer.setFont(font)
2957 lexer.setEolFill(False, -1)
2958 if hasattr(lexer, 'setStringsOverNewlineAllowed'):
2959 lexer.setStringsOverNewlineAllowed(False)
2960 table: List[Tuple[str, str]] = []
2961 aList = c.config.getData('qt-scintilla-styles')
2962 if aList:
2963 aList = [s.split(',') for s in aList]
2964 for z in aList:
2965 if len(z) == 2:
2966 color, style = z
2967 table.append((color.strip(), style.strip()),)
2968 else: g.trace(f"entry: {z}")
2969 if not table:
2970 black = '#000000'
2971 firebrick3 = '#CD2626'
2972 leo_green = '#00aa00'
2973 # See http://pyqt.sourceforge.net/Docs/QScintilla2/classQsciLexerPython.html
2974 # for list of selector names.
2975 table = [
2976 # EKR's personal settings are reasonable defaults.
2977 (black, 'ClassName'),
2978 (firebrick3, 'Comment'),
2979 (leo_green, 'Decorator'),
2980 (leo_green, 'DoubleQuotedString'),
2981 (black, 'FunctionMethodName'),
2982 ('blue', 'Keyword'),
2983 (black, 'Number'),
2984 (leo_green, 'SingleQuotedString'),
2985 (leo_green, 'TripleSingleQuotedString'),
2986 (leo_green, 'TripleDoubleQuotedString'),
2987 (leo_green, 'UnclosedString'),
2988 # End of line where string is not closed
2989 # style.python.13=fore:#000000,$(font.monospace),back:#E0C0E0,eolfilled
2990 ]
2991 for color, style in table:
2992 if hasattr(lexer, style):
2993 style_number = getattr(lexer, style)
2994 try:
2995 lexer.setColor(qcolor(color), style_number)
2996 except Exception:
2997 g.trace('bad color', color)
2998 else:
2999 pass
3000 # Not an error. Not all lexers have all styles.
3001 # g.trace('bad style: %s.%s' % (lexer.__class__.__name__, style))
3002 #@+node:ekr.20170128031840.1: *3* qsc.init
3003 def init(self, p):
3004 """QScintillaColorizer.init"""
3005 self.updateSyntaxColorer(p)
3006 self.changeLexer(self.language)
3007 #@+node:ekr.20170128133525.1: *3* qsc.makeLexersDict
3008 def makeLexersDict(self):
3009 """Make a dictionary of Scintilla lexers, and configure each one."""
3010 c = self.c
3011 # g.printList(sorted(dir(Qsci)))
3012 parent = c.frame.body.wrapper.widget
3013 table = (
3014 # 'Asm', 'Erlang', 'Forth', 'Haskell',
3015 # 'LaTeX', 'Lisp', 'Markdown', 'Nsis', 'R',
3016 'Bash', 'Batch', 'CPP', 'CSS', 'CMake', 'CSharp', 'CoffeeScript',
3017 'D', 'Diff', 'Fortran', 'Fortran77', 'HTML',
3018 'Java', 'JavaScript', 'Lua', 'Makefile', 'Matlab',
3019 'Pascal', 'Perl', 'Python', 'PostScript', 'Properties',
3020 'Ruby', 'SQL', 'TCL', 'TeX', 'XML', 'YAML',
3021 )
3022 d = {}
3023 for language_name in table:
3024 class_name = 'QsciLexer' + language_name
3025 lexer_class = getattr(Qsci, class_name, None)
3026 if lexer_class:
3027 # pylint: disable=not-callable
3028 lexer = lexer_class(parent=parent)
3029 self.configure_lexer(lexer)
3030 d[language_name.lower()] = lexer
3031 elif 0:
3032 g.trace('no lexer for', class_name)
3033 return d
3034 #@-others
3035#@+node:ekr.20190320062618.1: ** Jupyter classes
3036# Copyright (c) Jupyter Development Team.
3037# Distributed under the terms of the Modified BSD License.
3039if pygments:
3040 #@+others
3041 #@+node:ekr.20190320062624.2: *3* RegexLexer.get_tokens_unprocessed
3042 # Copyright (c) Jupyter Development Team.
3043 # Distributed under the terms of the Modified BSD License.
3045 from pygments.lexer import RegexLexer, _TokenType, Text, Error
3047 def get_tokens_unprocessed(self, text, stack=('root',)):
3048 """
3049 Split ``text`` into (tokentype, text) pairs.
3051 Monkeypatched to store the final stack on the object itself.
3053 The `text` parameter this gets passed is only the current line, so to
3054 highlight things like multiline strings correctly, we need to retrieve
3055 the state from the previous line (this is done in PygmentsHighlighter,
3056 below), and use it to continue processing the current line.
3057 """
3058 pos = 0
3059 tokendefs = self._tokens
3060 if hasattr(self, '_saved_state_stack'):
3061 statestack = list(self._saved_state_stack)
3062 else:
3063 statestack = list(stack)
3064 # Fix #1113...
3065 try:
3066 statetokens = tokendefs[statestack[-1]]
3067 except Exception:
3068 # g.es_exception()
3069 return
3070 while 1:
3071 for rexmatch, action, new_state in statetokens:
3072 m = rexmatch(text, pos)
3073 if m:
3074 if action is not None:
3075 # pylint: disable=unidiomatic-typecheck
3076 # EKR: Why not use isinstance?
3077 if type(action) is _TokenType:
3078 yield pos, action, m.group()
3079 else:
3080 for item in action(self, m):
3081 yield item
3082 pos = m.end()
3083 if new_state is not None:
3084 # state transition
3085 if isinstance(new_state, tuple):
3086 for state in new_state:
3087 if state == '#pop':
3088 statestack.pop()
3089 elif state == '#push':
3090 statestack.append(statestack[-1])
3091 else:
3092 statestack.append(state)
3093 elif isinstance(new_state, int):
3094 # pop
3095 del statestack[new_state:]
3096 elif new_state == '#push':
3097 statestack.append(statestack[-1])
3098 else:
3099 assert False, f"wrong state def: {new_state!r}"
3100 statetokens = tokendefs[statestack[-1]]
3101 break
3102 else:
3103 try:
3104 if text[pos] == '\n':
3105 # at EOL, reset state to "root"
3106 pos += 1
3107 statestack = ['root']
3108 statetokens = tokendefs['root']
3109 yield pos, Text, '\n'
3110 continue
3111 yield pos, Error, text[pos]
3112 pos += 1
3113 except IndexError:
3114 break
3115 self._saved_state_stack = list(statestack)
3117 # Monkeypatch!
3119 if pygments:
3120 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
3121 #@+node:ekr.20190320062624.3: *3* class PygmentsBlockUserData(QTextBlockUserData)
3122 # Copyright (c) Jupyter Development Team.
3123 # Distributed under the terms of the Modified BSD License.
3125 if QtGui:
3128 class PygmentsBlockUserData(QtGui.QTextBlockUserData): # type:ignore
3129 """ Storage for the user data associated with each line."""
3131 syntax_stack = ('root',)
3133 def __init__(self, **kwds):
3134 for key, value in kwds.items():
3135 setattr(self, key, value)
3136 super().__init__()
3138 def __repr__(self):
3139 attrs = ['syntax_stack']
3140 kwds = ', '.join([
3141 f"{attr}={getattr(self, attr)!r}"
3142 for attr in attrs
3143 ])
3144 return f"PygmentsBlockUserData({kwds})"
3145 #@-others
3146#@-others
3147#@@language python
3148#@@tabwidth -4
3149#@@pagewidth 70
3150#@-leo