• 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    builtin = r"([^.'\"\\#]\b|^)" + any("BUILTIN", builtinlist) + r"\b"
21    comment = any("COMMENT", [r"#[^\n]*"])
22    stringprefix = r"(?i:r|u|f|fr|rf|b|br|rb)?"
23    sqstring = stringprefix + r"'[^'\\\n]*(\\.[^'\\\n]*)*'?"
24    dqstring = stringprefix + r'"[^"\\\n]*(\\.[^"\\\n]*)*"?'
25    sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
26    dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
27    string = any("STRING", [sq3string, dq3string, sqstring, dqstring])
28    return kw + "|" + builtin + "|" + comment + "|" + string +\
29           "|" + any("SYNC", [r"\n"])
30
31prog = re.compile(make_pat(), re.S)
32idprog = re.compile(r"\s+(\w+)", re.S)
33
34def color_config(text):
35    """Set color options of Text widget.
36
37    If ColorDelegator is used, this should be called first.
38    """
39    # Called from htest, TextFrame, Editor, and Turtledemo.
40    # Not automatic because ColorDelegator does not know 'text'.
41    theme = idleConf.CurrentTheme()
42    normal_colors = idleConf.GetHighlight(theme, 'normal')
43    cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg')
44    select_colors = idleConf.GetHighlight(theme, 'hilite')
45    text.config(
46        foreground=normal_colors['foreground'],
47        background=normal_colors['background'],
48        insertbackground=cursor_color,
49        selectforeground=select_colors['foreground'],
50        selectbackground=select_colors['background'],
51        inactiveselectbackground=select_colors['background'],  # new in 8.5
52    )
53
54
55class ColorDelegator(Delegator):
56    """Delegator for syntax highlighting (text coloring).
57
58    Instance variables:
59        delegate: Delegator below this one in the stack, meaning the
60                one this one delegates to.
61
62        Used to track state:
63        after_id: Identifier for scheduled after event, which is a
64                timer for colorizing the text.
65        allow_colorizing: Boolean toggle for applying colorizing.
66        colorizing: Boolean flag when colorizing is in process.
67        stop_colorizing: Boolean flag to end an active colorizing
68                process.
69    """
70
71    def __init__(self):
72        Delegator.__init__(self)
73        self.init_state()
74        self.prog = prog
75        self.idprog = idprog
76        self.LoadTagDefs()
77
78    def init_state(self):
79        "Initialize variables that track colorizing state."
80        self.after_id = None
81        self.allow_colorizing = True
82        self.stop_colorizing = False
83        self.colorizing = False
84
85    def setdelegate(self, delegate):
86        """Set the delegate for this instance.
87
88        A delegate is an instance of a Delegator class and each
89        delegate points to the next delegator in the stack.  This
90        allows multiple delegators to be chained together for a
91        widget.  The bottom delegate for a colorizer is a Text
92        widget.
93
94        If there is a delegate, also start the colorizing process.
95        """
96        if self.delegate is not None:
97            self.unbind("<<toggle-auto-coloring>>")
98        Delegator.setdelegate(self, delegate)
99        if delegate is not None:
100            self.config_colors()
101            self.bind("<<toggle-auto-coloring>>", self.toggle_colorize_event)
102            self.notify_range("1.0", "end")
103        else:
104            # No delegate - stop any colorizing.
105            self.stop_colorizing = True
106            self.allow_colorizing = False
107
108    def config_colors(self):
109        "Configure text widget tags with colors from tagdefs."
110        for tag, cnf in self.tagdefs.items():
111            self.tag_configure(tag, **cnf)
112        self.tag_raise('sel')
113
114    def LoadTagDefs(self):
115        "Create dictionary of tag names to text colors."
116        theme = idleConf.CurrentTheme()
117        self.tagdefs = {
118            "COMMENT": idleConf.GetHighlight(theme, "comment"),
119            "KEYWORD": idleConf.GetHighlight(theme, "keyword"),
120            "BUILTIN": idleConf.GetHighlight(theme, "builtin"),
121            "STRING": idleConf.GetHighlight(theme, "string"),
122            "DEFINITION": idleConf.GetHighlight(theme, "definition"),
123            "SYNC": {'background':None,'foreground':None},
124            "TODO": {'background':None,'foreground':None},
125            "ERROR": idleConf.GetHighlight(theme, "error"),
126            # The following is used by ReplaceDialog:
127            "hit": idleConf.GetHighlight(theme, "hit"),
128            }
129
130        if DEBUG: print('tagdefs',self.tagdefs)
131
132    def insert(self, index, chars, tags=None):
133        "Insert chars into widget at index and mark for colorizing."
134        index = self.index(index)
135        self.delegate.insert(index, chars, tags)
136        self.notify_range(index, index + "+%dc" % len(chars))
137
138    def delete(self, index1, index2=None):
139        "Delete chars between indexes and mark for colorizing."
140        index1 = self.index(index1)
141        self.delegate.delete(index1, index2)
142        self.notify_range(index1)
143
144    def notify_range(self, index1, index2=None):
145        "Mark text changes for processing and restart colorizing, if active."
146        self.tag_add("TODO", index1, index2)
147        if self.after_id:
148            if DEBUG: print("colorizing already scheduled")
149            return
150        if self.colorizing:
151            self.stop_colorizing = True
152            if DEBUG: print("stop colorizing")
153        if self.allow_colorizing:
154            if DEBUG: print("schedule colorizing")
155            self.after_id = self.after(1, self.recolorize)
156        return
157
158    def close(self):
159        if self.after_id:
160            after_id = self.after_id
161            self.after_id = None
162            if DEBUG: print("cancel scheduled recolorizer")
163            self.after_cancel(after_id)
164        self.allow_colorizing = False
165        self.stop_colorizing = True
166
167    def toggle_colorize_event(self, event=None):
168        """Toggle colorizing on and off.
169
170        When toggling off, if colorizing is scheduled or is in
171        process, it will be cancelled and/or stopped.
172
173        When toggling on, colorizing will be scheduled.
174        """
175        if self.after_id:
176            after_id = self.after_id
177            self.after_id = None
178            if DEBUG: print("cancel scheduled recolorizer")
179            self.after_cancel(after_id)
180        if self.allow_colorizing and self.colorizing:
181            if DEBUG: print("stop colorizing")
182            self.stop_colorizing = True
183        self.allow_colorizing = not self.allow_colorizing
184        if self.allow_colorizing and not self.colorizing:
185            self.after_id = self.after(1, self.recolorize)
186        if DEBUG:
187            print("auto colorizing turned",\
188                  self.allow_colorizing and "on" or "off")
189        return "break"
190
191    def recolorize(self):
192        """Timer event (every 1ms) to colorize text.
193
194        Colorizing is only attempted when the text widget exists,
195        when colorizing is toggled on, and when the colorizing
196        process is not already running.
197
198        After colorizing is complete, some cleanup is done to
199        make sure that all the text has been colorized.
200        """
201        self.after_id = None
202        if not self.delegate:
203            if DEBUG: print("no delegate")
204            return
205        if not self.allow_colorizing:
206            if DEBUG: print("auto colorizing is off")
207            return
208        if self.colorizing:
209            if DEBUG: print("already colorizing")
210            return
211        try:
212            self.stop_colorizing = False
213            self.colorizing = True
214            if DEBUG: print("colorizing...")
215            t0 = time.perf_counter()
216            self.recolorize_main()
217            t1 = time.perf_counter()
218            if DEBUG: print("%.3f seconds" % (t1-t0))
219        finally:
220            self.colorizing = False
221        if self.allow_colorizing and self.tag_nextrange("TODO", "1.0"):
222            if DEBUG: print("reschedule colorizing")
223            self.after_id = self.after(1, self.recolorize)
224
225    def recolorize_main(self):
226        "Evaluate text and apply colorizing tags."
227        next = "1.0"
228        while True:
229            item = self.tag_nextrange("TODO", next)
230            if not item:
231                break
232            head, tail = item
233            self.tag_remove("SYNC", head, tail)
234            item = self.tag_prevrange("SYNC", head)
235            if item:
236                head = item[1]
237            else:
238                head = "1.0"
239
240            chars = ""
241            next = head
242            lines_to_get = 1
243            ok = False
244            while not ok:
245                mark = next
246                next = self.index(mark + "+%d lines linestart" %
247                                         lines_to_get)
248                lines_to_get = min(lines_to_get * 2, 100)
249                ok = "SYNC" in self.tag_names(next + "-1c")
250                line = self.get(mark, next)
251                ##print head, "get", mark, next, "->", repr(line)
252                if not line:
253                    return
254                for tag in self.tagdefs:
255                    self.tag_remove(tag, mark, next)
256                chars = chars + line
257                m = self.prog.search(chars)
258                while m:
259                    for key, value in m.groupdict().items():
260                        if value:
261                            a, b = m.span(key)
262                            self.tag_add(key,
263                                         head + "+%dc" % a,
264                                         head + "+%dc" % b)
265                            if value in ("def", "class"):
266                                m1 = self.idprog.match(chars, b)
267                                if m1:
268                                    a, b = m1.span(1)
269                                    self.tag_add("DEFINITION",
270                                                 head + "+%dc" % a,
271                                                 head + "+%dc" % b)
272                    m = self.prog.search(chars, m.end())
273                if "SYNC" in self.tag_names(next + "-1c"):
274                    head = next
275                    chars = ""
276                else:
277                    ok = False
278                if not ok:
279                    # We're in an inconsistent state, and the call to
280                    # update may tell us to stop.  It may also change
281                    # the correct value for "next" (since this is a
282                    # line.col string, not a true mark).  So leave a
283                    # crumb telling the next invocation to resume here
284                    # in case update tells us to leave.
285                    self.tag_add("TODO", next)
286                self.update()
287                if self.stop_colorizing:
288                    if DEBUG: print("colorizing stopped")
289                    return
290
291    def removecolors(self):
292        "Remove all colorizing tags."
293        for tag in self.tagdefs:
294            self.tag_remove(tag, "1.0", "end")
295
296
297def _color_delegator(parent):  # htest #
298    from tkinter import Toplevel, Text
299    from idlelib.percolator import Percolator
300
301    top = Toplevel(parent)
302    top.title("Test ColorDelegator")
303    x, y = map(int, parent.geometry().split('+')[1:])
304    top.geometry("700x250+%d+%d" % (x + 20, y + 175))
305    source = (
306        "if True: int ('1') # keyword, builtin, string, comment\n"
307        "elif False: print(0)\n"
308        "else: float(None)\n"
309        "if iF + If + IF: 'keyword matching must respect case'\n"
310        "if'': x or''  # valid string-keyword no-space combinations\n"
311        "async def f(): await g()\n"
312        "# All valid prefixes for unicode and byte strings should be colored.\n"
313        "'x', '''x''', \"x\", \"\"\"x\"\"\"\n"
314        "r'x', u'x', R'x', U'x', f'x', F'x'\n"
315        "fr'x', Fr'x', fR'x', FR'x', rf'x', rF'x', Rf'x', RF'x'\n"
316        "b'x',B'x', br'x',Br'x',bR'x',BR'x', rb'x', rB'x',Rb'x',RB'x'\n"
317        "# Invalid combinations of legal characters should be half colored.\n"
318        "ur'x', ru'x', uf'x', fu'x', UR'x', ufr'x', rfu'x', xf'x', fx'x'\n"
319        )
320    text = Text(top, background="white")
321    text.pack(expand=1, fill="both")
322    text.insert("insert", source)
323    text.focus_set()
324
325    color_config(text)
326    p = Percolator(text)
327    d = ColorDelegator()
328    p.insertfilter(d)
329
330
331if __name__ == "__main__":
332    from unittest import main
333    main('idlelib.idle_test.test_colorizer', verbosity=2, exit=False)
334
335    from idlelib.idle_test.htest import run
336    run(_color_delegator)
337