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