mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			384 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			384 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import builtins
 | |
| import keyword
 | |
| import re
 | |
| import time
 | |
| 
 | |
| from idlelib.config import idleConf
 | |
| from idlelib.delegator import Delegator
 | |
| 
 | |
| DEBUG = False
 | |
| 
 | |
| 
 | |
| def any(name, alternates):
 | |
|     "Return a named group pattern matching list of alternates."
 | |
|     return "(?P<%s>" % name + "|".join(alternates) + ")"
 | |
| 
 | |
| 
 | |
| def make_pat():
 | |
|     kw = r"\b" + any("KEYWORD", keyword.kwlist) + r"\b"
 | |
|     match_softkw = (
 | |
|         r"^[ \t]*" +  # at beginning of line + possible indentation
 | |
|         r"(?P<MATCH_SOFTKW>match)\b" +
 | |
|         r"(?![ \t]*(?:" + "|".join([  # not followed by ...
 | |
|             r"[:,;=^&|@~)\]}]",  # a character which means it can't be a
 | |
|                                  # pattern-matching statement
 | |
|             r"\b(?:" + r"|".join(keyword.kwlist) + r")\b",  # a keyword
 | |
|         ]) +
 | |
|         r"))"
 | |
|     )
 | |
|     case_default = (
 | |
|         r"^[ \t]*" +  # at beginning of line + possible indentation
 | |
|         r"(?P<CASE_SOFTKW>case)" +
 | |
|         r"[ \t]+(?P<CASE_DEFAULT_UNDERSCORE>_\b)"
 | |
|     )
 | |
|     case_softkw_and_pattern = (
 | |
|         r"^[ \t]*" +  # at beginning of line + possible indentation
 | |
|         r"(?P<CASE_SOFTKW2>case)\b" +
 | |
|         r"(?![ \t]*(?:" + "|".join([  # not followed by ...
 | |
|             r"_\b",  # a lone underscore
 | |
|             r"[:,;=^&|@~)\]}]",  # a character which means it can't be a
 | |
|                                  # pattern-matching case
 | |
|             r"\b(?:" + r"|".join(keyword.kwlist) + r")\b",  # a keyword
 | |
|         ]) +
 | |
|         r"))"
 | |
|     )
 | |
|     builtinlist = [str(name) for name in dir(builtins)
 | |
|                    if not name.startswith('_') and
 | |
|                    name not in keyword.kwlist]
 | |
|     builtin = r"([^.'\"\\#]\b|^)" + any("BUILTIN", builtinlist) + r"\b"
 | |
|     comment = any("COMMENT", [r"#[^\n]*"])
 | |
|     stringprefix = r"(?i:r|u|f|fr|rf|b|br|rb)?"
 | |
|     sqstring = stringprefix + r"'[^'\\\n]*(\\.[^'\\\n]*)*'?"
 | |
|     dqstring = stringprefix + r'"[^"\\\n]*(\\.[^"\\\n]*)*"?'
 | |
|     sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
 | |
|     dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
 | |
|     string = any("STRING", [sq3string, dq3string, sqstring, dqstring])
 | |
|     prog = re.compile("|".join([
 | |
|                                 builtin, comment, string, kw,
 | |
|                                 match_softkw, case_default,
 | |
|                                 case_softkw_and_pattern,
 | |
|                                 any("SYNC", [r"\n"]),
 | |
|                                ]),
 | |
|                       re.DOTALL | re.MULTILINE)
 | |
|     return prog
 | |
| 
 | |
| 
 | |
| prog = make_pat()
 | |
| idprog = re.compile(r"\s+(\w+)")
 | |
| prog_group_name_to_tag = {
 | |
|     "MATCH_SOFTKW": "KEYWORD",
 | |
|     "CASE_SOFTKW": "KEYWORD",
 | |
|     "CASE_DEFAULT_UNDERSCORE": "KEYWORD",
 | |
|     "CASE_SOFTKW2": "KEYWORD",
 | |
| }
 | |
| 
 | |
| 
 | |
| def matched_named_groups(re_match):
 | |
|     "Get only the non-empty named groups from an re.Match object."
 | |
|     return ((k, v) for (k, v) in re_match.groupdict().items() if v)
 | |
| 
 | |
| 
 | |
| def color_config(text):
 | |
|     """Set color options of Text widget.
 | |
| 
 | |
|     If ColorDelegator is used, this should be called first.
 | |
|     """
 | |
|     # Called from htest, TextFrame, Editor, and Turtledemo.
 | |
|     # Not automatic because ColorDelegator does not know 'text'.
 | |
|     theme = idleConf.CurrentTheme()
 | |
|     normal_colors = idleConf.GetHighlight(theme, 'normal')
 | |
|     cursor_color = idleConf.GetHighlight(theme, 'cursor')['foreground']
 | |
|     select_colors = idleConf.GetHighlight(theme, 'hilite')
 | |
|     text.config(
 | |
|         foreground=normal_colors['foreground'],
 | |
|         background=normal_colors['background'],
 | |
|         insertbackground=cursor_color,
 | |
|         selectforeground=select_colors['foreground'],
 | |
|         selectbackground=select_colors['background'],
 | |
|         inactiveselectbackground=select_colors['background'],  # new in 8.5
 | |
|         )
 | |
| 
 | |
| 
 | |
| class ColorDelegator(Delegator):
 | |
|     """Delegator for syntax highlighting (text coloring).
 | |
| 
 | |
|     Instance variables:
 | |
|         delegate: Delegator below this one in the stack, meaning the
 | |
|                 one this one delegates to.
 | |
| 
 | |
|         Used to track state:
 | |
|         after_id: Identifier for scheduled after event, which is a
 | |
|                 timer for colorizing the text.
 | |
|         allow_colorizing: Boolean toggle for applying colorizing.
 | |
|         colorizing: Boolean flag when colorizing is in process.
 | |
|         stop_colorizing: Boolean flag to end an active colorizing
 | |
|                 process.
 | |
|     """
 | |
| 
 | |
|     def __init__(self):
 | |
|         Delegator.__init__(self)
 | |
|         self.init_state()
 | |
|         self.prog = prog
 | |
|         self.idprog = idprog
 | |
|         self.LoadTagDefs()
 | |
| 
 | |
|     def init_state(self):
 | |
|         "Initialize variables that track colorizing state."
 | |
|         self.after_id = None
 | |
|         self.allow_colorizing = True
 | |
|         self.stop_colorizing = False
 | |
|         self.colorizing = False
 | |
| 
 | |
|     def setdelegate(self, delegate):
 | |
|         """Set the delegate for this instance.
 | |
| 
 | |
|         A delegate is an instance of a Delegator class and each
 | |
|         delegate points to the next delegator in the stack.  This
 | |
|         allows multiple delegators to be chained together for a
 | |
|         widget.  The bottom delegate for a colorizer is a Text
 | |
|         widget.
 | |
| 
 | |
|         If there is a delegate, also start the colorizing process.
 | |
|         """
 | |
|         if self.delegate is not None:
 | |
|             self.unbind("<<toggle-auto-coloring>>")
 | |
|         Delegator.setdelegate(self, delegate)
 | |
|         if delegate is not None:
 | |
|             self.config_colors()
 | |
|             self.bind("<<toggle-auto-coloring>>", self.toggle_colorize_event)
 | |
|             self.notify_range("1.0", "end")
 | |
|         else:
 | |
|             # No delegate - stop any colorizing.
 | |
|             self.stop_colorizing = True
 | |
|             self.allow_colorizing = False
 | |
| 
 | |
|     def config_colors(self):
 | |
|         "Configure text widget tags with colors from tagdefs."
 | |
|         for tag, cnf in self.tagdefs.items():
 | |
|             self.tag_configure(tag, **cnf)
 | |
|         self.tag_raise('sel')
 | |
| 
 | |
|     def LoadTagDefs(self):
 | |
|         "Create dictionary of tag names to text colors."
 | |
|         theme = idleConf.CurrentTheme()
 | |
|         self.tagdefs = {
 | |
|             "COMMENT": idleConf.GetHighlight(theme, "comment"),
 | |
|             "KEYWORD": idleConf.GetHighlight(theme, "keyword"),
 | |
|             "BUILTIN": idleConf.GetHighlight(theme, "builtin"),
 | |
|             "STRING": idleConf.GetHighlight(theme, "string"),
 | |
|             "DEFINITION": idleConf.GetHighlight(theme, "definition"),
 | |
|             "SYNC": {'background': None, 'foreground': None},
 | |
|             "TODO": {'background': None, 'foreground': None},
 | |
|             "ERROR": idleConf.GetHighlight(theme, "error"),
 | |
|             # "hit" is used by ReplaceDialog to mark matches. It shouldn't be changed by Colorizer, but
 | |
|             # that currently isn't technically possible. This should be moved elsewhere in the future
 | |
|             # when fixing the "hit" tag's visibility, or when the replace dialog is replaced with a
 | |
|             # non-modal alternative.
 | |
|             "hit": idleConf.GetHighlight(theme, "hit"),
 | |
|             }
 | |
|         if DEBUG: print('tagdefs', self.tagdefs)
 | |
| 
 | |
|     def insert(self, index, chars, tags=None):
 | |
|         "Insert chars into widget at index and mark for colorizing."
 | |
|         index = self.index(index)
 | |
|         self.delegate.insert(index, chars, tags)
 | |
|         self.notify_range(index, index + "+%dc" % len(chars))
 | |
| 
 | |
|     def delete(self, index1, index2=None):
 | |
|         "Delete chars between indexes and mark for colorizing."
 | |
|         index1 = self.index(index1)
 | |
|         self.delegate.delete(index1, index2)
 | |
|         self.notify_range(index1)
 | |
| 
 | |
|     def notify_range(self, index1, index2=None):
 | |
|         "Mark text changes for processing and restart colorizing, if active."
 | |
|         self.tag_add("TODO", index1, index2)
 | |
|         if self.after_id:
 | |
|             if DEBUG: print("colorizing already scheduled")
 | |
|             return
 | |
|         if self.colorizing:
 | |
|             self.stop_colorizing = True
 | |
|             if DEBUG: print("stop colorizing")
 | |
|         if self.allow_colorizing:
 | |
|             if DEBUG: print("schedule colorizing")
 | |
|             self.after_id = self.after(1, self.recolorize)
 | |
|         return
 | |
| 
 | |
|     def close(self):
 | |
|         if self.after_id:
 | |
|             after_id = self.after_id
 | |
|             self.after_id = None
 | |
|             if DEBUG: print("cancel scheduled recolorizer")
 | |
|             self.after_cancel(after_id)
 | |
|         self.allow_colorizing = False
 | |
|         self.stop_colorizing = True
 | |
| 
 | |
|     def toggle_colorize_event(self, event=None):
 | |
|         """Toggle colorizing on and off.
 | |
| 
 | |
|         When toggling off, if colorizing is scheduled or is in
 | |
|         process, it will be cancelled and/or stopped.
 | |
| 
 | |
|         When toggling on, colorizing will be scheduled.
 | |
|         """
 | |
|         if self.after_id:
 | |
|             after_id = self.after_id
 | |
|             self.after_id = None
 | |
|             if DEBUG: print("cancel scheduled recolorizer")
 | |
|             self.after_cancel(after_id)
 | |
|         if self.allow_colorizing and self.colorizing:
 | |
|             if DEBUG: print("stop colorizing")
 | |
|             self.stop_colorizing = True
 | |
|         self.allow_colorizing = not self.allow_colorizing
 | |
|         if self.allow_colorizing and not self.colorizing:
 | |
|             self.after_id = self.after(1, self.recolorize)
 | |
|         if DEBUG:
 | |
|             print("auto colorizing turned",
 | |
|                   "on" if self.allow_colorizing else "off")
 | |
|         return "break"
 | |
| 
 | |
|     def recolorize(self):
 | |
|         """Timer event (every 1ms) to colorize text.
 | |
| 
 | |
|         Colorizing is only attempted when the text widget exists,
 | |
|         when colorizing is toggled on, and when the colorizing
 | |
|         process is not already running.
 | |
| 
 | |
|         After colorizing is complete, some cleanup is done to
 | |
|         make sure that all the text has been colorized.
 | |
|         """
 | |
|         self.after_id = None
 | |
|         if not self.delegate:
 | |
|             if DEBUG: print("no delegate")
 | |
|             return
 | |
|         if not self.allow_colorizing:
 | |
|             if DEBUG: print("auto colorizing is off")
 | |
|             return
 | |
|         if self.colorizing:
 | |
|             if DEBUG: print("already colorizing")
 | |
|             return
 | |
|         try:
 | |
|             self.stop_colorizing = False
 | |
|             self.colorizing = True
 | |
|             if DEBUG: print("colorizing...")
 | |
|             t0 = time.perf_counter()
 | |
|             self.recolorize_main()
 | |
|             t1 = time.perf_counter()
 | |
|             if DEBUG: print("%.3f seconds" % (t1-t0))
 | |
|         finally:
 | |
|             self.colorizing = False
 | |
|         if self.allow_colorizing and self.tag_nextrange("TODO", "1.0"):
 | |
|             if DEBUG: print("reschedule colorizing")
 | |
|             self.after_id = self.after(1, self.recolorize)
 | |
| 
 | |
|     def recolorize_main(self):
 | |
|         "Evaluate text and apply colorizing tags."
 | |
|         next = "1.0"
 | |
|         while todo_tag_range := self.tag_nextrange("TODO", next):
 | |
|             self.tag_remove("SYNC", todo_tag_range[0], todo_tag_range[1])
 | |
|             sync_tag_range = self.tag_prevrange("SYNC", todo_tag_range[0])
 | |
|             head = sync_tag_range[1] if sync_tag_range else "1.0"
 | |
| 
 | |
|             chars = ""
 | |
|             next = head
 | |
|             lines_to_get = 1
 | |
|             ok = False
 | |
|             while not ok:
 | |
|                 mark = next
 | |
|                 next = self.index(mark + "+%d lines linestart" %
 | |
|                                          lines_to_get)
 | |
|                 lines_to_get = min(lines_to_get * 2, 100)
 | |
|                 ok = "SYNC" in self.tag_names(next + "-1c")
 | |
|                 line = self.get(mark, next)
 | |
|                 ##print head, "get", mark, next, "->", repr(line)
 | |
|                 if not line:
 | |
|                     return
 | |
|                 for tag in self.tagdefs:
 | |
|                     self.tag_remove(tag, mark, next)
 | |
|                 chars += line
 | |
|                 self._add_tags_in_section(chars, head)
 | |
|                 if "SYNC" in self.tag_names(next + "-1c"):
 | |
|                     head = next
 | |
|                     chars = ""
 | |
|                 else:
 | |
|                     ok = False
 | |
|                 if not ok:
 | |
|                     # We're in an inconsistent state, and the call to
 | |
|                     # update may tell us to stop.  It may also change
 | |
|                     # the correct value for "next" (since this is a
 | |
|                     # line.col string, not a true mark).  So leave a
 | |
|                     # crumb telling the next invocation to resume here
 | |
|                     # in case update tells us to leave.
 | |
|                     self.tag_add("TODO", next)
 | |
|                 self.update()
 | |
|                 if self.stop_colorizing:
 | |
|                     if DEBUG: print("colorizing stopped")
 | |
|                     return
 | |
| 
 | |
|     def _add_tag(self, start, end, head, matched_group_name):
 | |
|         """Add a tag to a given range in the text widget.
 | |
| 
 | |
|         This is a utility function, receiving the range as `start` and
 | |
|         `end` positions, each of which is a number of characters
 | |
|         relative to the given `head` index in the text widget.
 | |
| 
 | |
|         The tag to add is determined by `matched_group_name`, which is
 | |
|         the name of a regular expression "named group" as matched by
 | |
|         by the relevant highlighting regexps.
 | |
|         """
 | |
|         tag = prog_group_name_to_tag.get(matched_group_name,
 | |
|                                          matched_group_name)
 | |
|         self.tag_add(tag,
 | |
|                      f"{head}+{start:d}c",
 | |
|                      f"{head}+{end:d}c")
 | |
| 
 | |
|     def _add_tags_in_section(self, chars, head):
 | |
|         """Parse and add highlighting tags to a given part of the text.
 | |
| 
 | |
|         `chars` is a string with the text to parse and to which
 | |
|         highlighting is to be applied.
 | |
| 
 | |
|             `head` is the index in the text widget where the text is found.
 | |
|         """
 | |
|         for m in self.prog.finditer(chars):
 | |
|             for name, matched_text in matched_named_groups(m):
 | |
|                 a, b = m.span(name)
 | |
|                 self._add_tag(a, b, head, name)
 | |
|                 if matched_text in ("def", "class"):
 | |
|                     if m1 := self.idprog.match(chars, b):
 | |
|                         a, b = m1.span(1)
 | |
|                         self._add_tag(a, b, head, "DEFINITION")
 | |
| 
 | |
|     def removecolors(self):
 | |
|         "Remove all colorizing tags."
 | |
|         for tag in self.tagdefs:
 | |
|             self.tag_remove(tag, "1.0", "end")
 | |
| 
 | |
| 
 | |
| def _color_delegator(parent):  # htest #
 | |
|     from tkinter import Toplevel, Text
 | |
|     from idlelib.idle_test.test_colorizer import source
 | |
|     from idlelib.percolator import Percolator
 | |
| 
 | |
|     top = Toplevel(parent)
 | |
|     top.title("Test ColorDelegator")
 | |
|     x, y = map(int, parent.geometry().split('+')[1:])
 | |
|     top.geometry("700x550+%d+%d" % (x + 20, y + 175))
 | |
| 
 | |
|     text = Text(top, background="white")
 | |
|     text.pack(expand=1, fill="both")
 | |
|     text.insert("insert", source)
 | |
|     text.focus_set()
 | |
| 
 | |
|     color_config(text)
 | |
|     p = Percolator(text)
 | |
|     d = ColorDelegator()
 | |
|     p.insertfilter(d)
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     from unittest import main
 | |
|     main('idlelib.idle_test.test_colorizer', verbosity=2, exit=False)
 | |
| 
 | |
|     from idlelib.idle_test.htest import run
 | |
|     run(_color_delegator)
 | 
