1import builtins 2import keyword 3import re 4import time 5 6from idlelib.config import idleConf 7from idlelib.delegator import Delegator 8 9DEBUG = False 10 11def any(name, alternates): 12 "Return a named group pattern matching list of alternates." 13 return "(?P<%s>" % name + "|".join(alternates) + ")" 14 15def 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 33prog = re.compile(make_pat(), re.S) 34idprog = re.compile(r"\s+(\w+)", re.S) 35 36def 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 55class 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 257def _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 283if __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