blob: facbef8bb189191cc36d9e9300155d4ac150f35e [file] [log] [blame]
Georg Brandl1a3284e2007-12-02 09:40:06 +00001import builtins
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -04002import keyword
3import re
4import time
5
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -04006from idlelib.config import idleConf
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -04007from idlelib.delegator import Delegator
David Scherer7aced172000-08-15 01:13:23 +00008
Kurt B. Kaiser0bc3d982004-03-15 04:26:37 +00009DEBUG = False
David Scherer7aced172000-08-15 01:13:23 +000010
Thomas Wouters0e3f5912006-08-11 14:57:12 +000011def any(name, alternates):
12 "Return a named group pattern matching list of alternates."
13 return "(?P<%s>" % name + "|".join(alternates) + ")"
David Scherer7aced172000-08-15 01:13:23 +000014
15def make_pat():
16 kw = r"\b" + any("KEYWORD", keyword.kwlist) + r"\b"
Georg Brandl1a3284e2007-12-02 09:40:06 +000017 builtinlist = [str(name) for name in dir(builtins)
Terry Jan Reedydc224f82012-01-16 03:20:27 -050018 if not name.startswith('_') and \
19 name not in keyword.kwlist]
Thomas Wouters0e3f5912006-08-11 14:57:12 +000020 builtin = r"([^.'\"\\#]\b|^)" + any("BUILTIN", builtinlist) + r"\b"
David Scherer7aced172000-08-15 01:13:23 +000021 comment = any("COMMENT", [r"#[^\n]*"])
Terry Jan Reedyda585332018-04-02 01:47:46 -040022 stringprefix = r"(?i:r|u|f|fr|rf|b|br|rb)?"
Ned Deily5e92a1e2012-05-29 22:55:43 -070023 sqstring = stringprefix + r"'[^'\\\n]*(\\.[^'\\\n]*)*'?"
24 dqstring = stringprefix + r'"[^"\\\n]*(\\.[^"\\\n]*)*"?'
25 sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
26 dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
David Scherer7aced172000-08-15 01:13:23 +000027 string = any("STRING", [sq3string, dq3string, sqstring, dqstring])
Kurt B. Kaiser0bc3d982004-03-15 04:26:37 +000028 return kw + "|" + builtin + "|" + comment + "|" + string +\
29 "|" + any("SYNC", [r"\n"])
David Scherer7aced172000-08-15 01:13:23 +000030
31prog = re.compile(make_pat(), re.S)
32idprog = re.compile(r"\s+(\w+)", re.S)
33
Tal Einatc87d9f42018-09-23 15:23:15 +030034def color_config(text):
35 """Set color options of Text widget.
Terry Jan Reedy2bac3b72016-05-29 01:40:22 -040036
Tal Einatc87d9f42018-09-23 15:23:15 +030037 If ColorDelegator is used, this should be called first.
38 """
39 # Called from htest, TextFrame, Editor, and Turtledemo.
Terry Jan Reedy2bac3b72016-05-29 01:40:22 -040040 # 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'],
Terry Jan Reedy1080d132016-06-09 21:09:15 -040051 inactiveselectbackground=select_colors['background'], # new in 8.5
52 )
Terry Jan Reedy2bac3b72016-05-29 01:40:22 -040053
Tal Einatc87d9f42018-09-23 15:23:15 +030054
David Scherer7aced172000-08-15 01:13:23 +000055class ColorDelegator(Delegator):
Cheryl Sabellaee0f9272019-02-19 00:11:18 -050056 """Delegator for syntax highlighting (text coloring).
57
Cheryl Sabellaed1deb02019-02-27 08:21:16 -050058 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.
Cheryl Sabellaee0f9272019-02-19 00:11:18 -050065 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.
Cheryl Sabellaee0f9272019-02-19 00:11:18 -050069 """
David Scherer7aced172000-08-15 01:13:23 +000070
71 def __init__(self):
72 Delegator.__init__(self)
Cheryl Sabellaed1deb02019-02-27 08:21:16 -050073 self.init_state()
David Scherer7aced172000-08-15 01:13:23 +000074 self.prog = prog
75 self.idprog = idprog
Steven M. Gavab77d3432002-03-02 07:16:21 +000076 self.LoadTagDefs()
David Scherer7aced172000-08-15 01:13:23 +000077
Cheryl Sabellaed1deb02019-02-27 08:21:16 -050078 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
David Scherer7aced172000-08-15 01:13:23 +000085 def setdelegate(self, delegate):
Cheryl Sabellaee0f9272019-02-19 00:11:18 -050086 """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 """
David Scherer7aced172000-08-15 01:13:23 +000096 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")
Roger Serwy7733be82013-04-07 12:41:16 -0500103 else:
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500104 # No delegate - stop any colorizing.
Roger Serwy7733be82013-04-07 12:41:16 -0500105 self.stop_colorizing = True
106 self.allow_colorizing = False
David Scherer7aced172000-08-15 01:13:23 +0000107
108 def config_colors(self):
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500109 "Configure text widget tags with colors from tagdefs."
David Scherer7aced172000-08-15 01:13:23 +0000110 for tag, cnf in self.tagdefs.items():
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500111 self.tag_configure(tag, **cnf)
David Scherer7aced172000-08-15 01:13:23 +0000112 self.tag_raise('sel')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000113
Steven M. Gavab77d3432002-03-02 07:16:21 +0000114 def LoadTagDefs(self):
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500115 "Create dictionary of tag names to text colors."
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -0500116 theme = idleConf.CurrentTheme()
Steven M. Gavab77d3432002-03-02 07:16:21 +0000117 self.tagdefs = {
118 "COMMENT": idleConf.GetHighlight(theme, "comment"),
119 "KEYWORD": idleConf.GetHighlight(theme, "keyword"),
Kurt B. Kaiser73360a32004-03-08 18:15:31 +0000120 "BUILTIN": idleConf.GetHighlight(theme, "builtin"),
Steven M. Gavab77d3432002-03-02 07:16:21 +0000121 "STRING": idleConf.GetHighlight(theme, "string"),
122 "DEFINITION": idleConf.GetHighlight(theme, "definition"),
123 "SYNC": {'background':None,'foreground':None},
124 "TODO": {'background':None,'foreground':None},
Kurt B. Kaiser92b5ca32002-12-17 21:16:12 +0000125 "ERROR": idleConf.GetHighlight(theme, "error"),
Steven M. Gavab77d3432002-03-02 07:16:21 +0000126 # The following is used by ReplaceDialog:
127 "hit": idleConf.GetHighlight(theme, "hit"),
128 }
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000129
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000130 if DEBUG: print('tagdefs',self.tagdefs)
David Scherer7aced172000-08-15 01:13:23 +0000131
132 def insert(self, index, chars, tags=None):
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500133 "Insert chars into widget at index and mark for colorizing."
David Scherer7aced172000-08-15 01:13:23 +0000134 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):
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500139 "Delete chars between indexes and mark for colorizing."
David Scherer7aced172000-08-15 01:13:23 +0000140 index1 = self.index(index1)
141 self.delegate.delete(index1, index2)
142 self.notify_range(index1)
143
David Scherer7aced172000-08-15 01:13:23 +0000144 def notify_range(self, index1, index2=None):
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500145 "Mark text changes for processing and restart colorizing, if active."
David Scherer7aced172000-08-15 01:13:23 +0000146 self.tag_add("TODO", index1, index2)
147 if self.after_id:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000148 if DEBUG: print("colorizing already scheduled")
David Scherer7aced172000-08-15 01:13:23 +0000149 return
150 if self.colorizing:
Kurt B. Kaiser0bc3d982004-03-15 04:26:37 +0000151 self.stop_colorizing = True
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000152 if DEBUG: print("stop colorizing")
David Scherer7aced172000-08-15 01:13:23 +0000153 if self.allow_colorizing:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000154 if DEBUG: print("schedule colorizing")
David Scherer7aced172000-08-15 01:13:23 +0000155 self.after_id = self.after(1, self.recolorize)
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500156 return
David Scherer7aced172000-08-15 01:13:23 +0000157
Cheryl Sabellab9f03542019-03-01 05:19:40 -0500158 def close(self):
David Scherer7aced172000-08-15 01:13:23 +0000159 if self.after_id:
160 after_id = self.after_id
161 self.after_id = None
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000162 if DEBUG: print("cancel scheduled recolorizer")
David Scherer7aced172000-08-15 01:13:23 +0000163 self.after_cancel(after_id)
Kurt B. Kaiser0bc3d982004-03-15 04:26:37 +0000164 self.allow_colorizing = False
165 self.stop_colorizing = True
David Scherer7aced172000-08-15 01:13:23 +0000166
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500167 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 """
David Scherer7aced172000-08-15 01:13:23 +0000175 if self.after_id:
176 after_id = self.after_id
177 self.after_id = None
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000178 if DEBUG: print("cancel scheduled recolorizer")
David Scherer7aced172000-08-15 01:13:23 +0000179 self.after_cancel(after_id)
180 if self.allow_colorizing and self.colorizing:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000181 if DEBUG: print("stop colorizing")
Kurt B. Kaiser0bc3d982004-03-15 04:26:37 +0000182 self.stop_colorizing = True
David Scherer7aced172000-08-15 01:13:23 +0000183 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)
Kurt B. Kaiser6df4bf22001-07-13 00:04:24 +0000186 if DEBUG:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000187 print("auto colorizing turned",\
188 self.allow_colorizing and "on" or "off")
David Scherer7aced172000-08-15 01:13:23 +0000189 return "break"
190
191 def recolorize(self):
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500192 """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
Cheryl Sabellab9f03542019-03-01 05:19:40 -0500199 make sure that all the text has been colorized.
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500200 """
David Scherer7aced172000-08-15 01:13:23 +0000201 self.after_id = None
202 if not self.delegate:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000203 if DEBUG: print("no delegate")
David Scherer7aced172000-08-15 01:13:23 +0000204 return
205 if not self.allow_colorizing:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000206 if DEBUG: print("auto colorizing is off")
David Scherer7aced172000-08-15 01:13:23 +0000207 return
208 if self.colorizing:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000209 if DEBUG: print("already colorizing")
David Scherer7aced172000-08-15 01:13:23 +0000210 return
211 try:
Kurt B. Kaiser0bc3d982004-03-15 04:26:37 +0000212 self.stop_colorizing = False
213 self.colorizing = True
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000214 if DEBUG: print("colorizing...")
Victor Stinnerfe98e2f2012-04-29 03:01:20 +0200215 t0 = time.perf_counter()
David Scherer7aced172000-08-15 01:13:23 +0000216 self.recolorize_main()
Victor Stinnerfe98e2f2012-04-29 03:01:20 +0200217 t1 = time.perf_counter()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000218 if DEBUG: print("%.3f seconds" % (t1-t0))
David Scherer7aced172000-08-15 01:13:23 +0000219 finally:
Kurt B. Kaiser0bc3d982004-03-15 04:26:37 +0000220 self.colorizing = False
David Scherer7aced172000-08-15 01:13:23 +0000221 if self.allow_colorizing and self.tag_nextrange("TODO", "1.0"):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000222 if DEBUG: print("reschedule colorizing")
David Scherer7aced172000-08-15 01:13:23 +0000223 self.after_id = self.after(1, self.recolorize)
David Scherer7aced172000-08-15 01:13:23 +0000224
225 def recolorize_main(self):
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500226 "Evaluate text and apply colorizing tags."
David Scherer7aced172000-08-15 01:13:23 +0000227 next = "1.0"
Kurt B. Kaiser0bc3d982004-03-15 04:26:37 +0000228 while True:
David Scherer7aced172000-08-15 01:13:23 +0000229 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
Kurt B. Kaiser0bc3d982004-03-15 04:26:37 +0000243 ok = False
David Scherer7aced172000-08-15 01:13:23 +0000244 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)
Walter Dörwald70a6b492004-02-12 17:35:32 +0000251 ##print head, "get", mark, next, "->", repr(line)
David Scherer7aced172000-08-15 01:13:23 +0000252 if not line:
253 return
Kurt B. Kaisere0712772007-08-23 05:25:55 +0000254 for tag in self.tagdefs:
David Scherer7aced172000-08-15 01:13:23 +0000255 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:
Kurt B. Kaiser0bc3d982004-03-15 04:26:37 +0000277 ok = False
David Scherer7aced172000-08-15 01:13:23 +0000278 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:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000288 if DEBUG: print("colorizing stopped")
David Scherer7aced172000-08-15 01:13:23 +0000289 return
290
Kurt B. Kaiserdf506ea2005-06-12 04:33:30 +0000291 def removecolors(self):
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500292 "Remove all colorizing tags."
Kurt B. Kaisere0712772007-08-23 05:25:55 +0000293 for tag in self.tagdefs:
Kurt B. Kaiserdf506ea2005-06-12 04:33:30 +0000294 self.tag_remove(tag, "1.0", "end")
David Scherer7aced172000-08-15 01:13:23 +0000295
Terry Jan Reedy2bac3b72016-05-29 01:40:22 -0400296
Terry Jan Reedycd567362014-10-17 01:31:35 -0400297def _color_delegator(parent): # htest #
298 from tkinter import Toplevel, Text
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400299 from idlelib.percolator import Percolator
Terry Jan Reedycd567362014-10-17 01:31:35 -0400300
301 top = Toplevel(parent)
302 top.title("Test ColorDelegator")
Terry Jan Reedya7480322016-07-10 17:28:10 -0400303 x, y = map(int, parent.geometry().split('+')[1:])
Terry Jan Reedy0e102432017-01-01 21:21:39 -0500304 top.geometry("700x250+%d+%d" % (x + 20, y + 175))
Terry Jan Reedy55966f32018-04-02 11:18:02 -0400305 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"
Terry Jan Reedy389a48e2018-05-15 14:20:38 -0400311 "async def f(): await g()\n"
Terry Jan Reedyda585332018-04-02 01:47:46 -0400312 "# All valid prefixes for unicode and byte strings should be colored.\n"
Terry Jan Reedy246cbf22016-12-27 00:05:26 -0500313 "'x', '''x''', \"x\", \"\"\"x\"\"\"\n"
Terry Jan Reedyda585332018-04-02 01:47:46 -0400314 "r'x', u'x', R'x', U'x', f'x', F'x'\n"
Terry Jan Reedy246cbf22016-12-27 00:05:26 -0500315 "fr'x', Fr'x', fR'x', FR'x', rf'x', rF'x', Rf'x', RF'x'\n"
Cheryl Sabellaee0f9272019-02-19 00:11:18 -0500316 "b'x',B'x', br'x',Br'x',bR'x',BR'x', rb'x', rB'x',Rb'x',RB'x'\n"
Terry Jan Reedyda585332018-04-02 01:47:46 -0400317 "# Invalid combinations of legal characters should be half colored.\n"
Terry Jan Reedy389a48e2018-05-15 14:20:38 -0400318 "ur'x', ru'x', uf'x', fu'x', UR'x', ufr'x', rfu'x', xf'x', fx'x'\n"
Terry Jan Reedyda585332018-04-02 01:47:46 -0400319 )
Terry Jan Reedycd567362014-10-17 01:31:35 -0400320 text = Text(top, background="white")
David Scherer7aced172000-08-15 01:13:23 +0000321 text.pack(expand=1, fill="both")
Terry Jan Reedycd567362014-10-17 01:31:35 -0400322 text.insert("insert", source)
323 text.focus_set()
324
Terry Jan Reedy2bac3b72016-05-29 01:40:22 -0400325 color_config(text)
David Scherer7aced172000-08-15 01:13:23 +0000326 p = Percolator(text)
327 d = ColorDelegator()
328 p.insertfilter(d)
David Scherer7aced172000-08-15 01:13:23 +0000329
Tal Einatc87d9f42018-09-23 15:23:15 +0300330
David Scherer7aced172000-08-15 01:13:23 +0000331if __name__ == "__main__":
Terry Jan Reedyee5ef302018-06-15 18:20:55 -0400332 from unittest import main
333 main('idlelib.idle_test.test_colorizer', verbosity=2, exit=False)
Terry Jan Reedya7480322016-07-10 17:28:10 -0400334
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -0400335 from idlelib.idle_test.htest import run
336 run(_color_delegator)