Home | History | Annotate | Download | only in idlelib
      1 import builtins
      2 import keyword
      3 import re
      4 import time
      5 
      6 from idlelib.config import idleConf
      7 from idlelib.delegator import Delegator
      8 
      9 DEBUG = False
     10 
     11 def any(name, alternates):
     12     "Return a named group pattern matching list of alternates."
     13     return "(?P<%s>" % name + "|".join(alternates) + ")"
     14 
     15 def make_pat():
     16     kw = r"\b" + any("KEYWORD", keyword.kwlist) + r"\b"
     17     builtinlist = [str(name) for name in dir(builtins)
     18                                         if not name.startswith('_') and \
     19                                         name not in keyword.kwlist]
     20     # self.file = open("file") :
     21     # 1st 'file' colorized normal, 2nd as builtin, 3rd as string
     22     builtin = r"([^.'\"\\#]\b|^)" + any("BUILTIN", builtinlist) + r"\b"
     23     comment = any("COMMENT", [r"#[^\n]*"])
     24     stringprefix = r"(?i:\br|u|f|fr|rf|b|br|rb)?"
     25     sqstring = stringprefix + r"'[^'\\\n]*(\\.[^'\\\n]*)*'?"
     26     dqstring = stringprefix + r'"[^"\\\n]*(\\.[^"\\\n]*)*"?'
     27     sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
     28     dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
     29     string = any("STRING", [sq3string, dq3string, sqstring, dqstring])
     30     return kw + "|" + builtin + "|" + comment + "|" + string +\
     31            "|" + any("SYNC", [r"\n"])
     32 
     33 prog = re.compile(make_pat(), re.S)
     34 idprog = re.compile(r"\s+(\w+)", re.S)
     35 
     36 def color_config(text):  # Called from htest, Editor, and Turtle Demo.
     37     '''Set color opitons of Text widget.
     38 
     39     Should be called whenever ColorDelegator is called.
     40     '''
     41     # Not automatic because ColorDelegator does not know 'text'.
     42     theme = idleConf.CurrentTheme()
     43     normal_colors = idleConf.GetHighlight(theme, 'normal')
     44     cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg')
     45     select_colors = idleConf.GetHighlight(theme, 'hilite')
     46     text.config(
     47         foreground=normal_colors['foreground'],
     48         background=normal_colors['background'],
     49         insertbackground=cursor_color,
     50         selectforeground=select_colors['foreground'],
     51         selectbackground=select_colors['background'],
     52         inactiveselectbackground=select_colors['background'],  # new in 8.5
     53     )
     54 
     55 class ColorDelegator(Delegator):
     56 
     57     def __init__(self):
     58         Delegator.__init__(self)
     59         self.prog = prog
     60         self.idprog = idprog
     61         self.LoadTagDefs()
     62 
     63     def setdelegate(self, delegate):
     64         if self.delegate is not None:
     65             self.unbind("<<toggle-auto-coloring>>")
     66         Delegator.setdelegate(self, delegate)
     67         if delegate is not None:
     68             self.config_colors()
     69             self.bind("<<toggle-auto-coloring>>", self.toggle_colorize_event)
     70             self.notify_range("1.0", "end")
     71         else:
     72             # No delegate - stop any colorizing
     73             self.stop_colorizing = True
     74             self.allow_colorizing = False
     75 
     76     def config_colors(self):
     77         for tag, cnf in self.tagdefs.items():
     78             if cnf:
     79                 self.tag_configure(tag, **cnf)
     80         self.tag_raise('sel')
     81 
     82     def LoadTagDefs(self):
     83         theme = idleConf.CurrentTheme()
     84         self.tagdefs = {
     85             "COMMENT": idleConf.GetHighlight(theme, "comment"),
     86             "KEYWORD": idleConf.GetHighlight(theme, "keyword"),
     87             "BUILTIN": idleConf.GetHighlight(theme, "builtin"),
     88             "STRING": idleConf.GetHighlight(theme, "string"),
     89             "DEFINITION": idleConf.GetHighlight(theme, "definition"),
     90             "SYNC": {'background':None,'foreground':None},
     91             "TODO": {'background':None,'foreground':None},
     92             "ERROR": idleConf.GetHighlight(theme, "error"),
     93             # The following is used by ReplaceDialog:
     94             "hit": idleConf.GetHighlight(theme, "hit"),
     95             }
     96 
     97         if DEBUG: print('tagdefs',self.tagdefs)
     98 
     99     def insert(self, index, chars, tags=None):
    100         index = self.index(index)
    101         self.delegate.insert(index, chars, tags)
    102         self.notify_range(index, index + "+%dc" % len(chars))
    103 
    104     def delete(self, index1, index2=None):
    105         index1 = self.index(index1)
    106         self.delegate.delete(index1, index2)
    107         self.notify_range(index1)
    108 
    109     after_id = None
    110     allow_colorizing = True
    111     colorizing = False
    112 
    113     def notify_range(self, index1, index2=None):
    114         self.tag_add("TODO", index1, index2)
    115         if self.after_id:
    116             if DEBUG: print("colorizing already scheduled")
    117             return
    118         if self.colorizing:
    119             self.stop_colorizing = True
    120             if DEBUG: print("stop colorizing")
    121         if self.allow_colorizing:
    122             if DEBUG: print("schedule colorizing")
    123             self.after_id = self.after(1, self.recolorize)
    124 
    125     close_when_done = None # Window to be closed when done colorizing
    126 
    127     def close(self, close_when_done=None):
    128         if self.after_id:
    129             after_id = self.after_id
    130             self.after_id = None
    131             if DEBUG: print("cancel scheduled recolorizer")
    132             self.after_cancel(after_id)
    133         self.allow_colorizing = False
    134         self.stop_colorizing = True
    135         if close_when_done:
    136             if not self.colorizing:
    137                 close_when_done.destroy()
    138             else:
    139                 self.close_when_done = close_when_done
    140 
    141     def toggle_colorize_event(self, event):
    142         if self.after_id:
    143             after_id = self.after_id
    144             self.after_id = None
    145             if DEBUG: print("cancel scheduled recolorizer")
    146             self.after_cancel(after_id)
    147         if self.allow_colorizing and self.colorizing:
    148             if DEBUG: print("stop colorizing")
    149             self.stop_colorizing = True
    150         self.allow_colorizing = not self.allow_colorizing
    151         if self.allow_colorizing and not self.colorizing:
    152             self.after_id = self.after(1, self.recolorize)
    153         if DEBUG:
    154             print("auto colorizing turned",\
    155                   self.allow_colorizing and "on" or "off")
    156         return "break"
    157 
    158     def recolorize(self):
    159         self.after_id = None
    160         if not self.delegate:
    161             if DEBUG: print("no delegate")
    162             return
    163         if not self.allow_colorizing:
    164             if DEBUG: print("auto colorizing is off")
    165             return
    166         if self.colorizing:
    167             if DEBUG: print("already colorizing")
    168             return
    169         try:
    170             self.stop_colorizing = False
    171             self.colorizing = True
    172             if DEBUG: print("colorizing...")
    173             t0 = time.perf_counter()
    174             self.recolorize_main()
    175             t1 = time.perf_counter()
    176             if DEBUG: print("%.3f seconds" % (t1-t0))
    177         finally:
    178             self.colorizing = False
    179         if self.allow_colorizing and self.tag_nextrange("TODO", "1.0"):
    180             if DEBUG: print("reschedule colorizing")
    181             self.after_id = self.after(1, self.recolorize)
    182         if self.close_when_done:
    183             top = self.close_when_done
    184             self.close_when_done = None
    185             top.destroy()
    186 
    187     def recolorize_main(self):
    188         next = "1.0"
    189         while True:
    190             item = self.tag_nextrange("TODO", next)
    191             if not item:
    192                 break
    193             head, tail = item
    194             self.tag_remove("SYNC", head, tail)
    195             item = self.tag_prevrange("SYNC", head)
    196             if item:
    197                 head = item[1]
    198             else:
    199                 head = "1.0"
    200 
    201             chars = ""
    202             next = head
    203             lines_to_get = 1
    204             ok = False
    205             while not ok:
    206                 mark = next
    207                 next = self.index(mark + "+%d lines linestart" %
    208                                          lines_to_get)
    209                 lines_to_get = min(lines_to_get * 2, 100)
    210                 ok = "SYNC" in self.tag_names(next + "-1c")
    211                 line = self.get(mark, next)
    212                 ##print head, "get", mark, next, "->", repr(line)
    213                 if not line:
    214                     return
    215                 for tag in self.tagdefs:
    216                     self.tag_remove(tag, mark, next)
    217                 chars = chars + line
    218                 m = self.prog.search(chars)
    219                 while m:
    220                     for key, value in m.groupdict().items():
    221                         if value:
    222                             a, b = m.span(key)
    223                             self.tag_add(key,
    224                                          head + "+%dc" % a,
    225                                          head + "+%dc" % b)
    226                             if value in ("def", "class"):
    227                                 m1 = self.idprog.match(chars, b)
    228                                 if m1:
    229                                     a, b = m1.span(1)
    230                                     self.tag_add("DEFINITION",
    231                                                  head + "+%dc" % a,
    232                                                  head + "+%dc" % b)
    233                     m = self.prog.search(chars, m.end())
    234                 if "SYNC" in self.tag_names(next + "-1c"):
    235                     head = next
    236                     chars = ""
    237                 else:
    238                     ok = False
    239                 if not ok:
    240                     # We're in an inconsistent state, and the call to
    241                     # update may tell us to stop.  It may also change
    242                     # the correct value for "next" (since this is a
    243                     # line.col string, not a true mark).  So leave a
    244                     # crumb telling the next invocation to resume here
    245                     # in case update tells us to leave.
    246                     self.tag_add("TODO", next)
    247                 self.update()
    248                 if self.stop_colorizing:
    249                     if DEBUG: print("colorizing stopped")
    250                     return
    251 
    252     def removecolors(self):
    253         for tag in self.tagdefs:
    254             self.tag_remove(tag, "1.0", "end")
    255 
    256 
    257 def _color_delegator(parent):  # htest #
    258     from tkinter import Toplevel, Text
    259     from idlelib.percolator import Percolator
    260 
    261     top = Toplevel(parent)
    262     top.title("Test ColorDelegator")
    263     x, y = map(int, parent.geometry().split('+')[1:])
    264     top.geometry("700x250+%d+%d" % (x + 20, y + 175))
    265     source = ("# Following has syntax errors\n"
    266         "if True: then int 1\nelif False: print 0\nelse: float(None)\n"
    267         "if iF + If + IF: 'keywork matching must respect case'\n"
    268         "# All valid prefixes for unicode and byte strings should be colored\n"
    269         "'x', '''x''', \"x\", \"\"\"x\"\"\"\n"
    270         "r'x', u'x', R'x', U'x', f'x', F'x', ur'is invalid'\n"
    271         "fr'x', Fr'x', fR'x', FR'x', rf'x', rF'x', Rf'x', RF'x'\n"
    272         "b'x',B'x', br'x',Br'x',bR'x',BR'x', rb'x'.rB'x',Rb'x',RB'x'\n")
    273     text = Text(top, background="white")
    274     text.pack(expand=1, fill="both")
    275     text.insert("insert", source)
    276     text.focus_set()
    277 
    278     color_config(text)
    279     p = Percolator(text)
    280     d = ColorDelegator()
    281     p.insertfilter(d)
    282 
    283 if __name__ == "__main__":
    284     import unittest
    285     unittest.main('idlelib.idle_test.test_colorizer',
    286                   verbosity=2, exit=False)
    287 
    288     from idlelib.idle_test.htest import run
    289     run(_color_delegator)
    290