1import re
2import tkinter as tk
3from tkinter import ttk
4
5from thonny import get_workbench
6from thonny.languages import tr
7from thonny.ui_utils import SafeScrollbar
8
9
10class OutlineView(ttk.Frame):
11    def __init__(self, master):
12        ttk.Frame.__init__(self, master)
13        self._init_widgets()
14
15        self._tab_changed_binding = (
16            get_workbench()
17            .get_editor_notebook()
18            .bind("<<NotebookTabChanged>>", self._update_frame_contents, True)
19        )
20        get_workbench().bind("Save", self._update_frame_contents, True)
21        get_workbench().bind("SaveAs", self._update_frame_contents, True)
22        get_workbench().bind_class("Text", "<<NewLine>>", self._update_frame_contents, True)
23
24        self._update_frame_contents()
25
26    def destroy(self):
27        try:
28            # Not sure if editor notebook is still living
29            get_workbench().get_editor_notebook().unbind(
30                "<<NotebookTabChanged>>", self._tab_changed_binding
31            )
32        except Exception:
33            pass
34        self.vert_scrollbar["command"] = None
35        ttk.Frame.destroy(self)
36
37    def _init_widgets(self):
38        # init and place scrollbar
39        self.vert_scrollbar = SafeScrollbar(self, orient=tk.VERTICAL)
40        self.vert_scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
41
42        # init and place tree
43        self.tree = ttk.Treeview(self, yscrollcommand=self.vert_scrollbar.set)
44        self.tree.grid(row=0, column=0, sticky=tk.NSEW)
45        self.vert_scrollbar["command"] = self.tree.yview
46
47        # set single-cell frame
48        self.columnconfigure(0, weight=1)
49        self.rowconfigure(0, weight=1)
50
51        # init tree events
52        self.tree.bind("<<TreeviewSelect>>", self._on_select, True)
53        self.tree.bind("<Map>", self._update_frame_contents, True)
54
55        # configure the only tree column
56        self.tree.column("#0", anchor=tk.W, stretch=True)
57        # self.tree.heading('#0', text='Item (type @ line)', anchor=tk.W)
58        self.tree["show"] = ("tree",)
59
60        self._class_img = get_workbench().get_image("outline-class")
61        self._method_img = get_workbench().get_image("outline-method")
62
63    def _update_frame_contents(self, event=None):
64        if not self.winfo_ismapped():
65            return
66
67        self._clear_tree()
68
69        editor = get_workbench().get_editor_notebook().get_current_editor()
70        if editor is None:
71            return
72
73        root = self._parse_source(editor.get_code_view().get_content())
74        for child in root[2]:
75            self._add_item_to_tree("", child)
76
77    def _parse_source(self, source):
78        # all nodes in format (parent, node_indent, node_children, name, type, linenumber)
79        root_node = (None, 0, [], None, None, None)  # name, type and linenumber not needed for root
80        active_node = root_node
81
82        lineno = 0
83        for line in source.split("\n"):
84            lineno += 1
85            m = re.match(r"[ ]*[\w]{1}", line)
86            if m:
87                indent = len(m.group(0))
88                while indent <= active_node[1]:
89                    active_node = active_node[0]
90
91                t = re.match(
92                    r"[ \t]*(async[ \t]+)?(?P<type>(def|class){1})[ ]+(?P<name>[\w]+)", line
93                )
94                if t:
95                    current = (active_node, indent, [], t.group("name"), t.group("type"), lineno)
96                    active_node[2].append(current)
97                    active_node = current
98
99        return root_node
100
101    # adds a single item to the tree, recursively calls itself to add any child nodes
102    def _add_item_to_tree(self, parent, item):
103        # create the text to be played for this item
104        item_type = item[4]
105        item_text = " " + item[3]
106
107        if item_type == "class":
108            image = self._class_img
109        elif item_type == "def":
110            image = self._method_img
111        else:
112            image = None
113
114        # insert the item, set lineno as a 'hidden' value
115        current = self.tree.insert(parent, "end", text=item_text, values=item[5], image=image)
116
117        for child in item[2]:
118            self._add_item_to_tree(current, child)
119
120    # clears the tree by deleting all items
121    def _clear_tree(self):
122        for child_id in self.tree.get_children():
123            self.tree.delete(child_id)
124
125    def _on_select(self, event):
126        editor = get_workbench().get_editor_notebook().get_current_editor()
127        if editor:
128            code_view = editor.get_code_view()
129            focus = self.tree.focus()
130            if not focus:
131                return
132
133            values = self.tree.item(focus)["values"]
134            if not values:
135                return
136
137            lineno = values[0]
138            index = code_view.text.index(str(lineno) + ".0")
139            code_view.text.see(index)  # make sure that the double-clicked item is visible
140            code_view.text.select_lines(lineno, lineno)
141
142            get_workbench().event_generate(
143                "OutlineDoubleClick", item_text=self.tree.item(self.tree.focus(), option="text")
144            )
145
146
147def load_plugin() -> None:
148    get_workbench().add_view(OutlineView, tr("Outline"), "ne")
149