1"""
2NB! Stippling doesn't work on mac:
3http://wiki.tcl.tk/44444
4http://rkeene.org/projects/tcl/tk.fossil/tkthistory/2954673
5"""
6import logging
7import os.path
8from tkinter import font
9
10import thonny
11from thonny import get_workbench, jedi_utils
12from thonny.codeview import get_syntax_options_for_tag
13
14
15def create_bitmap_file(width, height, predicate, name):
16
17    cache_dir = os.path.join(thonny.THONNY_USER_DIR, "image_cache")
18    name = "%s_%d_%d.xbm" % (name, width, height)
19    filename = os.path.join(cache_dir, name)
20
21    # if os.path.exists(filename):
22    #    return filename
23
24    hex_lines = []
25
26    if width % 8 == 0:
27        row_size = width
28    else:
29        # need to pad row size so that it is multiple of 8
30        row_size = width + 8 - (width % 8)
31
32    for y in range(height):
33        byte_hexes = []
34        for byte_index in range(row_size // 8):
35            byte = 0
36            for bit_index in range(7, -1, -1):
37                x = byte_index * 8 + bit_index
38
39                byte <<= 1
40                if predicate(x, y):
41                    byte |= 1
42
43            byte_hexes.append(format(byte, "#04x"))
44        hex_lines.append(",".join(byte_hexes))
45
46    data = (
47        "#define im_width %d\n" % width
48        + "#define im_height %d\n" % height
49        + "static char im_bits[] = {\n"
50        + "%s\n" % ",\n".join(hex_lines)
51        + "};"
52    )
53
54    os.makedirs(cache_dir, exist_ok=True)
55    with open(filename, "w") as fp:
56        fp.write(data)
57    return filename
58
59
60def configure_text(text):
61    spacing1 = 2
62    spacing3 = 3
63    text_font = text["font"]
64    text.configure(spacing1=spacing1, spacing3=spacing3)
65    text.master._gutter.configure(spacing1=spacing1, spacing3=spacing3)
66    if isinstance(text_font, str):
67        text_font = font.nametofont(text_font)
68
69    indent_width = text_font.measure("    ")
70    bbox = text.bbox("1.0")
71    if bbox is None or bbox[3] < 5:
72        # text not ready yet
73        # TODO: Text in Tk 8.6 has sync method
74        return False
75
76    line_height = bbox[3] + spacing1 + spacing3
77
78    print(indent_width, line_height)
79
80    def ver(x: int, y: int, top: bool, bottom: bool) -> bool:
81        # tells where to show pixels in vertical border of the statement
82        # It would be convenient if tiling started from the start of
83        # 1st char, but it is offset a bit
84        # In order to make computation easier, I'm offsetting x as well
85        x = (x - 5) % indent_width
86
87        stripe_width = 8
88        gap = 3
89        left = indent_width - stripe_width - gap
90
91        return (
92            left <= x < left + stripe_width
93            or top
94            and y == 0
95            and x >= left
96            or bottom
97            and y == line_height - 1
98            and x >= left
99        )
100
101    def hor(x: int, y: int, top: bool, bottom: bool) -> bool:
102        # tells where to show pixels in statement line
103        return top and y == 0 or bottom and y == line_height - 1
104
105    color = get_syntax_options_for_tag("GUTTER").get("background", "gray")
106    for orient, base_predicate in [("hor", hor), ("ver", ver)]:
107        for top in [False, True]:
108            for bottom in [False, True]:
109
110                def predicate(
111                    x,
112                    y,
113                    # need to make base_predicate, top and bottom local
114                    base_predicate=base_predicate,
115                    top=top,
116                    bottom=bottom,
117                ):
118                    return base_predicate(x, y, top, bottom)
119
120                tag_name = "%s_%s_%s" % (orient, top, bottom)
121                bitmap_path = create_bitmap_file(indent_width, line_height, predicate, tag_name)
122                text.tag_configure(tag_name, background=color, bgstipple="@" + bitmap_path)
123
124    return True
125
126
127def print_tree(node, level=0):
128    from parso.python import tree as python_tree
129
130    indent = "  " * level
131    # if (isinstance(node, python_tree.PythonNode) and node.type == "sim"
132    if node.type in ("simple_stmt",) or isinstance(node, python_tree.Flow):
133        print(indent, node.type, node.start_pos, node.end_pos)
134
135    if hasattr(node, "children"):
136        for child in node.children:
137            print_tree(child, level + 1)
138
139
140def clear_tags(text):
141    for pos in ["ver", "hor"]:
142        for top in [True, False]:
143            for bottom in [True, False]:
144                text.tag_remove("%s_%s_%s" % (pos, top, bottom), "1.0", "end")
145
146
147def add_tags(text):
148    source = text.get("1.0", "end")
149    clear_tags(text)
150    tree = jedi_utils.parse_source(source)
151
152    print_tree(tree)
153    last_line = 0
154    last_col = 0
155
156    def tag_tree(node):
157        nonlocal last_line, last_col
158        from parso.python import tree as python_tree
159
160        if node.type == "simple_stmt" or isinstance(node, (python_tree.Flow, python_tree.Scope)):
161
162            start_line, start_col = node.start_pos
163            end_line, end_col = node.end_pos
164
165            # Before dealing with this node,
166            # handle the case, where last vertical tag was meant for
167            # same column, but there were empty or comment lines between
168            if start_col == last_col:
169                for i in range(last_line + 1, start_line):
170                    # NB! tag not visible when logically empty line
171                    # doesn't have indent prefix
172                    text.tag_add(
173                        "ver_False_False", "%d.%d" % (i, last_col - 1), "%d.%d" % (i, last_col)
174                    )
175                    print("ver_False_False", "%d.%d" % (i, last_col - 1), "%d.%d" % (i, last_col))
176
177            print(node)
178
179            # usually end_col is 0
180            # exceptions: several statements on the same line (semicoloned statements)
181            # also unclosed parens in if-header
182            for lineno in range(start_line, end_line if end_col == 0 else end_line + 1):
183
184                top = lineno == start_line and lineno > 1
185                bottom = False  # start_line == end_line-1
186
187                # horizontal line (only for first or last line)
188                if top or bottom:
189                    text.tag_add(
190                        "hor_%s_%s" % (top, bottom),
191                        "%d.%d" % (lineno, start_col),
192                        "%d.%d" % (lineno + 1 if end_col == 0 else lineno, 0),
193                    )
194
195                    print(
196                        "hor_%s_%s" % (top, bottom),
197                        "%d.%d" % (lineno, start_col),
198                        "%d.%d" % (lineno + 1, 0),
199                    )
200
201                # vertical line (only for indented statements)
202                # Note that I'm using start col for all lines
203                # (statement's indent shouldn't decrease in continuation lines)
204                if start_col > 0:
205                    text.tag_add(
206                        "ver_%s_%s" % (top, bottom),
207                        "%d.%d" % (lineno, start_col - 1),
208                        "%d.%d" % (lineno, start_col),
209                    )
210                    print(
211                        "ver_%s_%s" % (top, bottom),
212                        "%d.%d" % (lineno, start_col - 1),
213                        "%d.%d" % (lineno, start_col),
214                    )
215
216                    last_line = lineno
217                    last_col = start_col
218
219        # Recurse
220        if node.type != "simple_stmt" and hasattr(node, "children"):
221            for child in node.children:
222                tag_tree(child)
223
224    tag_tree(tree)
225
226
227def handle_editor_event(event):
228    configure_and_add_tags(event.editor.get_text_widget())
229
230
231def handle_events(event):
232    if hasattr(event, "text_widget"):
233        text = event.text_widget
234    else:
235        text = event.widget
236
237    configure_and_add_tags(text)
238
239
240def configure_and_add_tags(text):
241    if not getattr(text, "structure_tags_configured", False):
242        try:
243            if configure_text(text):
244                text.structure_tags_configured = True
245            else:
246                text.after(500, lambda: configure_and_add_tags(text))
247                return
248        except Exception:
249            logging.exception("Problem with defining structure tags")
250            return
251
252    add_tags(text)
253
254
255def _load_plugin() -> None:
256    wb = get_workbench()
257
258    wb.set_default("view.program_structure", False)
259    wb.bind("Save", handle_editor_event, True)
260    wb.bind("Open", handle_editor_event, True)
261    wb.bind_class("CodeViewText", "<<TextChange>>", handle_events, True)
262