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