• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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