summaryrefslogtreecommitdiff
path: root/Lib/idlelib/colorizer.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/idlelib/colorizer.py')
-rw-r--r--Lib/idlelib/colorizer.py289
1 files changed, 289 insertions, 0 deletions
diff --git a/Lib/idlelib/colorizer.py b/Lib/idlelib/colorizer.py
new file mode 100644
index 0000000000..ff40845288
--- /dev/null
+++ b/Lib/idlelib/colorizer.py
@@ -0,0 +1,289 @@
+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"
+ builtinlist = [str(name) for name in dir(builtins)
+ if not name.startswith('_') and \
+ name not in keyword.kwlist]
+ # self.file = open("file") :
+ # 1st 'file' colorized normal, 2nd as builtin, 3rd as string
+ builtin = r"([^.'\"\\#]\b|^)" + any("BUILTIN", builtinlist) + r"\b"
+ comment = any("COMMENT", [r"#[^\n]*"])
+ stringprefix = r"(?i:\br|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])
+ return kw + "|" + builtin + "|" + comment + "|" + string +\
+ "|" + any("SYNC", [r"\n"])
+
+prog = re.compile(make_pat(), re.S)
+idprog = re.compile(r"\s+(\w+)", re.S)
+
+def color_config(text): # Called from htest, Editor, and Turtle Demo.
+ '''Set color opitons of Text widget.
+
+ Should be called whenever ColorDelegator is called.
+ '''
+ # Not automatic because ColorDelegator does not know 'text'.
+ theme = idleConf.CurrentTheme()
+ normal_colors = idleConf.GetHighlight(theme, 'normal')
+ cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg')
+ 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):
+
+ def __init__(self):
+ Delegator.__init__(self)
+ self.prog = prog
+ self.idprog = idprog
+ self.LoadTagDefs()
+
+ def setdelegate(self, delegate):
+ 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):
+ for tag, cnf in self.tagdefs.items():
+ if cnf:
+ self.tag_configure(tag, **cnf)
+ self.tag_raise('sel')
+
+ def LoadTagDefs(self):
+ 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"),
+ # The following is used by ReplaceDialog:
+ "hit": idleConf.GetHighlight(theme, "hit"),
+ }
+
+ if DEBUG: print('tagdefs',self.tagdefs)
+
+ def insert(self, index, chars, tags=None):
+ index = self.index(index)
+ self.delegate.insert(index, chars, tags)
+ self.notify_range(index, index + "+%dc" % len(chars))
+
+ def delete(self, index1, index2=None):
+ index1 = self.index(index1)
+ self.delegate.delete(index1, index2)
+ self.notify_range(index1)
+
+ after_id = None
+ allow_colorizing = True
+ colorizing = False
+
+ def notify_range(self, index1, index2=None):
+ 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)
+
+ close_when_done = None # Window to be closed when done colorizing
+
+ def close(self, close_when_done=None):
+ 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
+ if close_when_done:
+ if not self.colorizing:
+ close_when_done.destroy()
+ else:
+ self.close_when_done = close_when_done
+
+ def toggle_colorize_event(self, event):
+ 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",\
+ self.allow_colorizing and "on" or "off")
+ return "break"
+
+ def recolorize(self):
+ 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)
+ if self.close_when_done:
+ top = self.close_when_done
+ self.close_when_done = None
+ top.destroy()
+
+ def recolorize_main(self):
+ next = "1.0"
+ while True:
+ item = self.tag_nextrange("TODO", next)
+ if not item:
+ break
+ head, tail = item
+ self.tag_remove("SYNC", head, tail)
+ item = self.tag_prevrange("SYNC", head)
+ if item:
+ head = item[1]
+ else:
+ head = "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 = chars + line
+ m = self.prog.search(chars)
+ while m:
+ for key, value in m.groupdict().items():
+ if value:
+ a, b = m.span(key)
+ self.tag_add(key,
+ head + "+%dc" % a,
+ head + "+%dc" % b)
+ if value in ("def", "class"):
+ m1 = self.idprog.match(chars, b)
+ if m1:
+ a, b = m1.span(1)
+ self.tag_add("DEFINITION",
+ head + "+%dc" % a,
+ head + "+%dc" % b)
+ m = self.prog.search(chars, m.end())
+ 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 removecolors(self):
+ for tag in self.tagdefs:
+ self.tag_remove(tag, "1.0", "end")
+
+
+def _color_delegator(parent): # htest #
+ from tkinter import Toplevel, Text
+ from idlelib.percolator import Percolator
+
+ top = Toplevel(parent)
+ top.title("Test ColorDelegator")
+ x, y = map(int, parent.geometry().split('+')[1:])
+ top.geometry("700x250+%d+%d" % (x + 20, y + 175))
+ source = ("# Following has syntax errors\n"
+ "if True: then int 1\nelif False: print 0\nelse: float(None)\n"
+ "if iF + If + IF: 'keywork matching must respect case'\n"
+ "# All valid prefixes for unicode and byte strings should be colored\n"
+ "'x', '''x''', \"x\", \"\"\"x\"\"\"\n"
+ "r'x', u'x', R'x', U'x', f'x', F'x', ur'is invalid'\n"
+ "fr'x', Fr'x', fR'x', FR'x', rf'x', rF'x', Rf'x', RF'x'\n"
+ "b'x',B'x', br'x',Br'x',bR'x',BR'x', rb'x'.rB'x',Rb'x',RB'x'\n")
+ 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__":
+ import unittest
+ unittest.main('idlelib.idle_test.test_colorizer',
+ verbosity=2, exit=False)
+
+ from idlelib.idle_test.htest import run
+ run(_color_delegator)