1# -*- coding: utf-8 -*-
2
3"""
4Adds debugging commands and features.
5"""
6
7import ast
8import logging
9import os.path
10import tkinter as tk
11from tkinter import ttk
12from tkinter.messagebox import showinfo
13from typing import List, Union  # @UnusedImport
14
15from _tkinter import TclError
16
17from thonny import (
18    ast_utils,
19    editors,
20    get_runner,
21    get_workbench,
22    memory,
23    misc_utils,
24    running,
25    ui_utils,
26)
27from thonny.codeview import CodeView, SyntaxText, get_syntax_options_for_tag
28from thonny.common import DebuggerCommand, InlineCommand
29from thonny.languages import tr
30from thonny.memory import VariablesFrame
31from thonny.misc_utils import running_on_mac_os, running_on_rpi, shorten_repr
32from thonny.tktextext import TextFrame
33from thonny.ui_utils import CommonDialog, select_sequence
34from thonny.ui_utils import select_sequence, CommonDialog, get_tk_version_info
35from _tkinter import TclError
36
37_current_debugger = None
38
39RESUME_COMMAND_CAPTION = ""  # Init later when gettext is loaded
40
41
42class Debugger:
43    def __init__(self):
44        self._last_progress_message = None
45        self._last_brought_out_frame_id = None
46        self._editor_context_menu = None
47
48    def check_issue_command(self, command, **kwargs):
49        cmd = DebuggerCommand(command, **kwargs)
50        self._last_debugger_command = cmd
51
52        if get_runner().is_waiting_debugger_command():
53            logging.debug("_check_issue_debugger_command: %s", cmd)
54
55            # tell MainCPythonBackend the state we are seeing
56            cmd.setdefault(
57                frame_id=self._last_progress_message.stack[-1].id,
58                breakpoints=self.get_effective_breakpoints(command),
59                state=self._last_progress_message.stack[-1].event,
60                focus=self._last_progress_message.stack[-1].focus,
61                allow_stepping_into_libraries=get_workbench().get_option(
62                    "debugger.allow_stepping_into_libraries"
63                ),
64            )
65            if command == "run_to_cursor":
66                # cursor position was added as another breakpoint
67                cmd.name = "resume"
68
69            get_runner().send_command(cmd)
70            if command == "resume":
71                self.clear_last_frame()
72        else:
73            logging.debug("Bad state for sending debugger command " + str(command))
74
75    def get_run_to_cursor_breakpoint(self):
76        return None
77
78    def get_effective_breakpoints(self, command):
79        result = editors.get_current_breakpoints()
80
81        if command == "run_to_cursor":
82            bp = self.get_run_to_cursor_breakpoint()
83            if bp is not None:
84                filename, lineno = bp
85                result.setdefault(filename, set())
86                result[filename].add(lineno)
87
88        return result
89
90    def command_enabled(self, command):
91        if not get_runner().is_waiting_debugger_command():
92            return False
93
94        if command == "run_to_cursor":
95            return self.get_run_to_cursor_breakpoint() is not None
96        elif command == "step_back":
97            return (
98                self._last_progress_message
99                and self._last_progress_message["tracer_class"] == "NiceTracer"
100            )
101        else:
102            return True
103
104    def handle_debugger_progress(self, msg):
105        self._last_brought_out_frame_id = None
106
107    def handle_debugger_return(self, msg):
108        pass
109
110    def close(self) -> None:
111        self._last_brought_out_frame_id = None
112
113        if get_workbench().get_option("debugger.automatic_stack_view"):
114            get_workbench().hide_view("StackView")
115
116    def clear_last_frame(self):
117        pass
118
119    def get_frame_by_id(self, frame_id):
120        for frame_info in self._last_progress_message.stack:
121            if frame_info.id == frame_id:
122                return frame_info
123
124        raise ValueError("Could not find frame %d" % frame_id)
125
126    def bring_out_frame(self, frame_id, force=False):
127        # called by StackView
128        raise NotImplementedError()
129
130    def get_editor_context_menu(self):
131        def create_edit_command_handler(virtual_event_sequence):
132            def handler(event=None):
133                widget = get_workbench().focus_get()
134                if widget:
135                    return widget.event_generate(virtual_event_sequence)
136
137                return None
138
139            return handler
140
141        if self._editor_context_menu is None:
142            menu = tk.Menu(get_workbench())
143            menu.add(
144                "command",
145                label=tr("Run to cursor"),
146                command=lambda: self.check_issue_command("run_to_cursor"),
147            )
148            menu.add("separator")
149            menu.add("command", label="Copy", command=create_edit_command_handler("<<Copy>>"))
150            menu.add(
151                "command",
152                label=tr("Select all"),
153                command=create_edit_command_handler("<<SelectAll>>"),
154            )
155            self._editor_context_menu = menu
156
157        return self._editor_context_menu
158
159
160class SingleWindowDebugger(Debugger):
161    def __init__(self):
162        super().__init__()
163        self._last_frame_visualizer = None
164        # Make sure StackView is created
165        get_workbench().get_view("StackView")
166
167    def get_run_to_cursor_breakpoint(self):
168        editor = get_workbench().get_editor_notebook().get_current_editor()
169        if editor:
170            filename = editor.get_filename()
171            selection = editor.get_code_view().get_selected_range()
172            lineno = selection.lineno
173            if filename and lineno:
174                return filename, lineno
175
176        return None
177
178    def handle_debugger_progress(self, msg):
179        super().handle_debugger_progress(msg)
180        self._last_progress_message = msg
181        self.bring_out_frame(self._last_progress_message.stack[-1].id, force=True)
182
183        if get_workbench().get_option("debugger.automatic_stack_view"):
184            if len(msg.stack) > 1:
185                get_workbench().show_view("StackView")
186
187        get_workbench().get_view("ExceptionView").set_exception(
188            msg["exception_info"]["lines_with_frame_info"]
189        )
190
191    def close(self):
192        super().close()
193        if self._last_frame_visualizer is not None:
194            self._last_frame_visualizer.close()
195            self._last_frame_visualizer = None
196
197    def clear_last_frame(self):
198        if self._last_frame_visualizer is not None:
199            self._last_frame_visualizer.clear()
200
201    def bring_out_frame(self, frame_id, force=False):
202        if not force and frame_id == self._last_brought_out_frame_id:
203            return
204
205        self._last_brought_out_frame_id = frame_id
206
207        frame_info = self.get_frame_by_id(frame_id)
208
209        if (
210            self._last_frame_visualizer is not None
211            and self._last_frame_visualizer._frame_id != frame_info.id
212        ):
213            self._last_frame_visualizer.close()
214            self._last_frame_visualizer = None
215
216        if self._last_frame_visualizer is None:
217            self._last_frame_visualizer = EditorVisualizer(frame_info)
218
219        self._last_frame_visualizer._update_this_frame(self._last_progress_message, frame_info)
220
221        # show variables
222        var_view = get_workbench().get_view("VariablesView")
223        if frame_info.code_name == "<module>":
224            var_view.show_globals(frame_info.globals, frame_info.module_name)
225        else:
226            var_view.show_frame_variables(
227                frame_info.locals,
228                frame_info.globals,
229                frame_info.freevars,
230                frame_info.module_name
231                if frame_info.code_name == "<module>"
232                else frame_info.code_name,
233            )
234
235    def handle_debugger_return(self, msg):
236        if (
237            self._last_frame_visualizer is not None
238            and self._last_frame_visualizer.get_frame_id() == msg.get("frame_id")
239        ):
240            self._last_frame_visualizer.close()
241
242
243class StackedWindowsDebugger(Debugger):
244    def __init__(self):
245        super().__init__()
246        self._main_frame_visualizer = None
247
248    def get_run_to_cursor_breakpoint(self):
249        visualizer = self._get_topmost_selected_visualizer()
250        if visualizer:
251            assert isinstance(visualizer._text_frame, CodeView)
252            code_view = visualizer._text_frame
253            selection = code_view.get_selected_range()
254
255            target_lineno = visualizer._firstlineno - 1 + selection.lineno
256            return visualizer._filename, target_lineno
257        else:
258            return None
259
260    def handle_debugger_progress(self, msg):
261        super().handle_debugger_progress(msg)
262
263        self._last_progress_message = msg
264
265        main_frame_id = msg.stack[0].id
266
267        # clear obsolete main frame visualizer
268        if (
269            self._main_frame_visualizer
270            and self._main_frame_visualizer.get_frame_id() != main_frame_id
271        ):
272            self._main_frame_visualizer.close()
273            self._main_frame_visualizer = None
274
275        if not self._main_frame_visualizer:
276            self._main_frame_visualizer = EditorVisualizer(msg.stack[0])
277
278        self._main_frame_visualizer.update_this_and_next_frames(msg)
279
280        self.bring_out_frame(msg.stack[-1].id, force=True)
281
282        get_workbench().get_view("ExceptionView").set_exception(
283            msg["exception_info"]["lines_with_frame_info"]
284        )
285
286    def close(self):
287        super().close()
288        if self._main_frame_visualizer is not None:
289            self._main_frame_visualizer.close()
290            self._main_frame_visualizer = None
291
292    def clear_last_frame(self):
293        visualizer = self._get_topmost_visualizer()
294        if visualizer is not None:
295            visualizer.clear()
296
297    def _get_topmost_visualizer(self):
298        visualizer = self._main_frame_visualizer
299        if visualizer is None:
300            return None
301
302        while visualizer._next_frame_visualizer is not None:
303            visualizer = visualizer._next_frame_visualizer
304
305        return visualizer
306
307    def _get_topmost_selected_visualizer(self):
308        visualizer = self._get_topmost_visualizer()
309        if visualizer is None:
310            return None
311
312        topmost_text_widget = visualizer._text
313        focused_widget = get_workbench().focus_get()
314
315        if focused_widget is None:
316            return None
317        elif focused_widget == topmost_text_widget:
318            return visualizer
319        else:
320            return None
321
322    def bring_out_frame(self, frame_id, force=False):
323        if not force and frame_id == self._last_brought_out_frame_id:
324            return
325
326        self._last_brought_out_frame_id = frame_id
327
328        self._main_frame_visualizer.bring_out_frame(frame_id)
329
330        # show variables
331        var_view = get_workbench().get_view("VariablesView")
332        frame_info = self.get_frame_by_id(frame_id)
333        var_view.show_globals(frame_info.globals, frame_info.module_name)
334
335    def handle_debugger_return(self, msg):
336        if self._main_frame_visualizer is None:
337            return
338
339        self._main_frame_visualizer.close(msg["frame_id"])
340        if msg["frame_id"] == self._main_frame_visualizer.get_frame_id():
341            self._main_frame_visualizer = None
342
343
344class FrameVisualizer:
345    """
346    Is responsible for stepping through statements and updating corresponding UI
347    in Editor-s, FunctionCallDialog-s, ModuleDialog-s
348    """
349
350    def __init__(self, text_frame, frame_info):
351        self._text_frame = text_frame
352        self._text = text_frame.text
353        self._frame_info = frame_info
354        self._frame_id = frame_info.id
355        self._filename = frame_info.filename
356        self._firstlineno = None
357        if running_on_mac_os():
358            self._expression_box = ToplevelExpressionBox(text_frame)
359        else:
360            self._expression_box = PlacedExpressionBox(text_frame)
361
362        self._note_box = ui_utils.NoteBox(text_frame.winfo_toplevel())
363        self._next_frame_visualizer = None
364        self._prev_frame_visualizer = None
365        self._text.set_read_only(True)
366        self._line_debug = frame_info.current_statement is None
367
368        self._reconfigure_tags()
369
370    def _translate_lineno(self, lineno):
371        return lineno - self._firstlineno + 1
372
373    def _reconfigure_tags(self):
374        for tag in ["active_focus", "exception_focus"]:
375            conf = get_syntax_options_for_tag(tag).copy()
376            if self._line_debug:
377                # meaning data comes from line-debug
378                conf["borderwidth"] = 0
379
380            self._text.tag_configure(tag, **conf)
381
382    def close(self, frame_id=None):
383        if self._next_frame_visualizer:
384            self._next_frame_visualizer.close(frame_id)
385            if frame_id is None or self._next_frame_visualizer.get_frame_id() == frame_id:
386                self._next_frame_visualizer = None
387
388        if frame_id is None or frame_id == self._frame_id:
389            self._text.set_read_only(False)
390            self.clear()
391            # self._expression_box.destroy()
392
393    def clear(self):
394        self.remove_focus_tags()
395        self.hide_expression_box()
396        self.close_note()
397
398    def get_frame_id(self):
399        return self._frame_id
400
401    def update_this_and_next_frames(self, msg):
402        """Must not be used on obsolete frame"""
403
404        # debug("State: %s, focus: %s", msg.state, msg.focus)
405
406        frame_info, next_frame_info = self._find_this_and_next_frame(msg.stack)
407        self._update_this_frame(msg, frame_info)
408
409        # clear obsolete next frame visualizer
410        if self._next_frame_visualizer and (
411            not next_frame_info or self._next_frame_visualizer.get_frame_id() != next_frame_info.id
412        ):
413            self._next_frame_visualizer.close()
414            self._next_frame_visualizer = None
415
416        if next_frame_info and not self._next_frame_visualizer:
417            self._next_frame_visualizer = self._create_next_frame_visualizer(next_frame_info)
418            self._next_frame_visualizer._prev_frame_visualizer = self
419
420        if self._next_frame_visualizer:
421            self._next_frame_visualizer.update_this_and_next_frames(msg)
422
423    def remove_focus_tags(self):
424        for name in [
425            "exception_focus",
426            "active_focus",
427            "completed_focus",
428            "suspended_focus",
429            "sel",
430        ]:
431            self._text.tag_remove(name, "0.0", "end")
432
433    def hide_expression_box(self):
434        if self._expression_box is not None:
435            self._expression_box.clear_debug_view()
436
437    def _update_this_frame(self, msg, frame_info):
438        self._frame_info = frame_info
439        self.remove_focus_tags()
440
441        if frame_info.event == "line":
442            if (
443                frame_info.id in msg["exception_info"]["affected_frame_ids"]
444                and msg["exception_info"]["is_fresh"]
445            ):
446                self._tag_range(frame_info.focus, "exception_focus")
447            else:
448                self._tag_range(frame_info.focus, "active_focus")
449        else:
450            if "statement" in frame_info.event:
451                if msg["exception_info"]["msg"] is not None and msg["exception_info"]["is_fresh"]:
452                    stmt_tag = "exception_focus"
453                elif frame_info.event.startswith("before"):
454                    stmt_tag = "active_focus"
455                else:
456                    stmt_tag = "completed_focus"
457            else:
458                assert "expression" in frame_info.event
459                stmt_tag = "suspended_focus"
460
461            if frame_info.current_statement is not None:
462                self._tag_range(frame_info.current_statement, stmt_tag)
463            else:
464                logging.warning("Missing current_statement: %s", frame_info)
465
466        self._expression_box.update_expression(msg, frame_info)
467
468        if (
469            frame_info.id in msg["exception_info"]["affected_frame_ids"]
470            and msg["exception_info"]["is_fresh"]
471        ):
472            self._show_exception(msg["exception_info"]["lines_with_frame_info"], frame_info)
473        else:
474            self.close_note()
475
476    def _show_exception(self, lines, frame_info):
477        last_line_text = lines[-1][0]
478        self.show_note(
479            last_line_text.strip() + " ",
480            ("...", lambda _: get_workbench().show_view("ExceptionView")),
481            focus=frame_info.focus,
482        )
483
484    def _find_this_and_next_frame(self, stack):
485        for i in range(len(stack)):
486            if stack[i].id == self._frame_id:
487                if i == len(stack) - 1:  # last frame
488                    return stack[i], None
489                else:
490                    return stack[i], stack[i + 1]
491
492        raise AssertionError("Frame doesn't exist anymore")
493
494    def _tag_range(self, text_range, tag):
495        # For most statements I want to highlight block of whole lines
496        # but for pseudo-statements (like header in for-loop) I want to highlight only the indicated range
497
498        self._text.tag_raise(tag)
499
500        line_prefix = self._text.get(
501            "%d.0" % self._translate_lineno(text_range.lineno),
502            "%d.%d" % (self._translate_lineno(text_range.lineno), text_range.col_offset),
503        )
504        if line_prefix.strip():
505            # pseudo-statement
506            first_line = self._translate_lineno(text_range.lineno)
507            last_line = self._translate_lineno(text_range.end_lineno)
508            self._text.tag_add(
509                tag,
510                "%d.%d" % (first_line, text_range.col_offset),
511                "%d.%d" % (last_line, text_range.end_col_offset),
512            )
513        else:
514            # normal statement
515            first_line, first_col, last_line = self._get_text_range_block(text_range)
516
517            for lineno in range(first_line, last_line + 1):
518                self._text.tag_add(tag, "%d.%d" % (lineno, first_col), "%d.0" % (lineno + 1))
519
520        self._text.update_idletasks()
521        self._text.see("%d.0" % (last_line))
522        self._text.see("%d.0" % (first_line))
523
524        if last_line - first_line < 3:
525            # if it's safe to assume that whole code fits into screen
526            # then scroll it down a bit so that expression view doesn't hide behind
527            # lower edge of the editor
528            self._text.update_idletasks()
529            self._text.see("%d.0" % (first_line + 3))
530
531    def _get_text_range_block(self, text_range):
532        first_line = text_range.lineno - self._firstlineno + 1
533        last_line = (
534            text_range.end_lineno - self._firstlineno + (1 if text_range.end_col_offset > 0 else 0)
535        )
536        first_line_content = self._text.get("%d.0" % first_line, "%d.end" % first_line)
537        if first_line_content.strip().startswith("elif "):
538            first_col = first_line_content.find("elif ")
539        else:
540            first_col = text_range.col_offset
541
542        return (first_line, first_col, last_line)
543
544    def _create_next_frame_visualizer(self, next_frame_info):
545        if next_frame_info.code_name == "<module>":
546            return ModuleLoadDialog(self._text, next_frame_info)
547        else:
548            dialog = FunctionCallDialog(self._text.master, next_frame_info)
549
550            if self._expression_box.winfo_ismapped():
551                dialog.title(self._expression_box.get_focused_text())
552            else:
553                dialog.title(tr("Function call at %s") % hex(self._frame_id))
554
555            return dialog
556
557    def bring_out_frame(self, frame_id):
558        if self._frame_id == frame_id:
559            self.bring_out_this_frame()
560        elif self._next_frame_visualizer is not None:
561            self._next_frame_visualizer.bring_out_frame(frame_id)
562
563    def bring_out_this_frame(self):
564        pass
565
566    def show_note(self, *content_items: Union[str, List], target=None, focus=None) -> None:
567        if target is None:
568            target = self._text
569
570        self._note_box.show_note(*content_items, target=target, focus=focus)
571
572    def close_note(self):
573        self._note_box.close()
574
575
576class EditorVisualizer(FrameVisualizer):
577    """
578    Takes care of stepping in the editor
579    (main module in case of StackedWindowsDebugger)
580    """
581
582    def __init__(self, frame_info):
583        self.editor = (
584            get_workbench().get_editor_notebook().show_file(frame_info.filename, set_focus=False)
585        )
586        FrameVisualizer.__init__(self, self.editor.get_code_view(), frame_info)
587        self._firstlineno = 1
588
589    def _update_this_frame(self, msg, frame_info):
590        FrameVisualizer._update_this_frame(self, msg, frame_info)
591        if msg.in_present:
592            self._decorate_editor_title("")
593        else:
594            self._decorate_editor_title("   <<< REPLAYING >>> ")
595
596    def _decorate_editor_title(self, suffix):
597        self.editor.master.update_editor_title(self.editor, self.editor.get_title() + suffix)
598
599    def bring_out_this_frame(self):
600        get_workbench().focus_set()
601
602    def clear(self):
603        super().clear()
604        self._decorate_editor_title("")
605
606
607class BaseExpressionBox:
608    def __init__(self, codeview, text):
609        self.text = text
610
611        self._codeview = codeview
612
613        self._last_focus = None
614        self._last_root_expression = None
615
616        self.text.tag_configure("value", get_syntax_options_for_tag("value"))
617        self.text.tag_configure("before", get_syntax_options_for_tag("active_focus"))
618        self.text.tag_configure("after", get_syntax_options_for_tag("completed_focus"))
619        self.text.tag_configure("exception", get_syntax_options_for_tag("exception_focus"))
620        self.text.tag_raise("exception", "before")
621        self.text.tag_raise("exception", "after")
622
623    def get_text_options(self):
624        opts = dict(
625            height=1,
626            width=1,
627            relief=tk.RAISED,
628            background="#DCEDF2",
629            borderwidth=1,
630            highlightthickness=0,
631            padx=7,
632            pady=7,
633            wrap=tk.NONE,
634            font="EditorFont",
635        )
636        opts.update(get_syntax_options_for_tag("expression_box"))
637        return opts
638
639    def update_expression(self, msg, frame_info):
640        focus = frame_info.focus
641        event = frame_info.event
642
643        if frame_info.current_root_expression is not None:
644
645            if self._last_root_expression != frame_info.current_root_expression:
646                # can happen, eg. when focus jumps from the last expr in while body
647                # to while test expression
648                self.clear_debug_view()
649
650            with open(frame_info.filename, "rb") as fp:
651                whole_source = fp.read()
652
653            lines = whole_source.splitlines()
654            if len(lines) < frame_info.current_root_expression.end_lineno:
655                # it must be on a synthetical line which is not actually present in the editor
656                self.clear_debug_view()
657                return
658
659            self._load_expression(
660                whole_source, frame_info.filename, frame_info.current_root_expression
661            )
662            for subrange, value in frame_info.current_evaluations:
663                self._replace(subrange, value)
664            if "expression" in event:
665                # Event may be also after_statement_again
666                self._highlight_range(
667                    focus,
668                    event,
669                    (
670                        frame_info.id in msg["exception_info"]["affected_frame_ids"]
671                        and msg["exception_info"]["is_fresh"]
672                    ),
673                )
674                self._last_focus = focus
675
676            self._update_position(frame_info.current_root_expression)
677            self._update_size()
678
679        else:
680            # hide and clear on non-expression events
681            self.clear_debug_view()
682
683        self._last_root_expression = frame_info.current_root_expression
684
685    def get_focused_text(self):
686        if self._last_focus:
687            start_mark = self._get_mark_name(self._last_focus.lineno, self._last_focus.col_offset)
688            end_mark = self._get_mark_name(
689                self._last_focus.end_lineno, self._last_focus.end_col_offset
690            )
691            return self.text.get(start_mark, end_mark)
692        else:
693            return ""
694
695    def clear_debug_view(self):
696        self._main_range = None
697        self._last_focus = None
698        self._clear_expression()
699
700    def _clear_expression(self):
701        for tag in self.text.tag_names():
702            self.text.tag_remove(tag, "1.0", "end")
703
704        self.text.mark_unset(*self.text.mark_names())
705        self.text.delete("1.0", "end")
706
707    def _replace(self, focus, value):
708        start_mark = self._get_mark_name(focus.lineno, focus.col_offset)
709        end_mark = self._get_mark_name(focus.end_lineno, focus.end_col_offset)
710
711        self.text.delete(start_mark, end_mark)
712
713        id_str = memory.format_object_id(value.id)
714        if get_workbench().in_heap_mode():
715            value_str = id_str
716        else:
717            value_str = shorten_repr(value.repr, 100)
718
719        object_tag = "object_" + str(value.id)
720        self.text.insert(start_mark, value_str, ("value", object_tag))
721        if misc_utils.running_on_mac_os():
722            sequence = "<Command-Button-1>"
723        else:
724            sequence = "<Control-Button-1>"
725        self.text.tag_bind(
726            object_tag,
727            sequence,
728            lambda _: get_workbench().event_generate("ObjectSelect", object_id=value.id),
729        )
730
731    def _load_expression(self, whole_source, filename, text_range):
732
733        root = ast_utils.parse_source(whole_source, filename)
734        main_node = ast_utils.find_expression(root, text_range)
735
736        source = ast_utils.extract_text_range(whole_source, text_range)
737        logging.debug("EV.load_exp: %s", (text_range, main_node, source))
738
739        self._clear_expression()
740
741        self.text.insert("1.0", source)
742
743        # create node marks
744        def _create_index(lineno, col_offset):
745            local_lineno = lineno - main_node.lineno + 1
746            if lineno == main_node.lineno:
747                local_col_offset = col_offset - main_node.col_offset
748            else:
749                local_col_offset = col_offset
750
751            return str(local_lineno) + "." + str(local_col_offset)
752
753        for node in ast.walk(main_node):
754            if "lineno" in node._attributes and hasattr(node, "end_lineno"):
755                index1 = _create_index(node.lineno, node.col_offset)
756                index2 = _create_index(node.end_lineno, node.end_col_offset)
757
758                start_mark = self._get_mark_name(node.lineno, node.col_offset)
759                if not start_mark in self.text.mark_names():
760                    self.text.mark_set(start_mark, index1)
761                    # print("Creating mark", start_mark, index1)
762                    self.text.mark_gravity(start_mark, tk.LEFT)
763
764                end_mark = self._get_mark_name(node.end_lineno, node.end_col_offset)
765                if not end_mark in self.text.mark_names():
766                    self.text.mark_set(end_mark, index2)
767                    # print("Creating mark", end_mark, index2)
768                    self.text.mark_gravity(end_mark, tk.RIGHT)
769
770    def _get_mark_name(self, lineno, col_offset):
771        return str(lineno) + "_" + str(col_offset)
772
773    def _get_tag_name(self, node_or_text_range):
774        return (
775            str(node_or_text_range.lineno)
776            + "_"
777            + str(node_or_text_range.col_offset)
778            + "_"
779            + str(node_or_text_range.end_lineno)
780            + "_"
781            + str(node_or_text_range.end_col_offset)
782        )
783
784    def _highlight_range(self, text_range, state, has_exception):
785        logging.debug("EV._highlight_range: %s", text_range)
786        self.text.tag_remove("after", "1.0", "end")
787        self.text.tag_remove("before", "1.0", "end")
788        self.text.tag_remove("exception", "1.0", "end")
789
790        if state.startswith("after"):
791            tag = "after"
792        elif state.startswith("before"):
793            tag = "before"
794        else:
795            return
796
797        start_index = self._get_mark_name(text_range.lineno, text_range.col_offset)
798        end_index = self._get_mark_name(text_range.end_lineno, text_range.end_col_offset)
799        self.text.tag_add(tag, start_index, end_index)
800
801        if has_exception:
802            self.text.tag_add("exception", start_index, end_index)
803
804    def _update_position(self, text_range):
805        self._codeview.update_idletasks()
806        text_line_number = text_range.lineno - self._codeview._first_line_number + 1
807        bbox = self._codeview.text.bbox(str(text_line_number) + "." + str(text_range.col_offset))
808
809        if isinstance(bbox, tuple):
810            x = bbox[0] - self._codeview.text.cget("padx") + 6
811            y = bbox[1] - self._codeview.text.cget("pady") - 6
812        else:
813            x = 30
814            y = 30
815
816        self._set_position_make_visible(x, y)
817
818    def _update_size(self):
819        content = self.text.get("1.0", tk.END)
820        lines = content.splitlines()
821        self.text["height"] = len(lines)
822        self.text["width"] = max(map(len, lines))
823
824    def _set_position_make_visible(self, rel_x, rel_y):
825        raise NotImplementedError()
826
827
828class PlacedExpressionBox(BaseExpressionBox, tk.Text):
829    def __init__(self, codeview):
830        tk.Text.__init__(self, codeview.winfo_toplevel(), self.get_text_options())
831        BaseExpressionBox.__init__(self, codeview, self)
832
833    def clear_debug_view(self):
834        if self.winfo_ismapped():
835            self.place_forget()
836
837        super().clear_debug_view()
838
839    def _set_position_make_visible(self, rel_x, rel_y):
840        x = rel_x
841        y = rel_y
842
843        widget = self._codeview.text
844        while widget != self.master:
845            x += widget.winfo_x()
846            y += widget.winfo_y()
847            widget = widget.master
848
849        if not self.winfo_ismapped():
850            self.place(x=x, y=y, anchor=tk.NW)
851            self.update()
852
853
854class ToplevelExpressionBox(BaseExpressionBox, tk.Toplevel):
855    def __init__(self, codeview):
856        tk.Toplevel.__init__(self, codeview.winfo_toplevel())
857        text = tk.Text(self, **self.get_text_options())
858        BaseExpressionBox.__init__(self, codeview, text)
859        self.text.grid()
860
861        if running_on_mac_os():
862            try:
863                # NB! Must be the first thing to do after creation
864                # https://wiki.tcl-lang.org/page/MacWindowStyle
865                self.tk.call(
866                    "::tk::unsupported::MacWindowStyle", "style", self._w, "help", "noActivates"
867                )
868            except TclError:
869                pass
870        else:
871            raise RuntimeError("Should be used only on Mac")
872
873        self.resizable(False, False)
874        if get_tk_version_info() >= (8, 6, 10) and running_on_mac_os():
875            # self.wm_overrideredirect(1) # Tkinter wrapper can give error in 3.9.2
876            self.tk.call("wm", "overrideredirect", self._w, 1)
877        self.wm_transient(codeview.winfo_toplevel())
878        self.lift()
879
880    def clear_debug_view(self):
881        if self.winfo_ismapped():
882            self.withdraw()
883
884        super().clear_debug_view()
885
886    def _set_position_make_visible(self, rel_x, rel_y):
887        """
888        widget = self._codeview.text
889        while widget is not None:
890            x += widget.winfo_x()
891            y += widget.winfo_y()
892            widget = widget.master
893        """
894        x = rel_x + self._codeview.text.winfo_rootx()
895        y = rel_y + self._codeview.text.winfo_rooty()
896
897        if not self.winfo_ismapped():
898            self.update()
899            self.deiconify()
900        self.geometry("+%d+%d" % (x, y))
901
902    def get_text_options(self):
903        opts = super().get_text_options()
904        opts["relief"] = "flat"
905        opts["borderwidth"] = 0
906        return opts
907
908
909class DialogVisualizer(CommonDialog, FrameVisualizer):
910    def __init__(self, master, frame_info):
911        CommonDialog.__init__(self, master)
912
913        self.transient(master)
914        if misc_utils.running_on_windows():
915            self.wm_attributes("-toolwindow", 1)
916
917        # TODO: take size from prefs
918        editor_notebook = get_workbench().get_editor_notebook()
919        if master.winfo_toplevel() == get_workbench():
920            position_reference = editor_notebook
921        else:
922            # align to previous frame
923            position_reference = master.winfo_toplevel()
924
925        self.geometry(
926            "{}x{}+{}+{}".format(
927                editor_notebook.winfo_width(),
928                editor_notebook.winfo_height() - 20,
929                position_reference.winfo_rootx(),
930                position_reference.winfo_rooty(),
931            )
932        )
933        self.protocol("WM_DELETE_WINDOW", self._on_close)
934        self.bind("<FocusIn>", self._on_focus, True)
935
936        self._init_layout_widgets(master, frame_info)
937        FrameVisualizer.__init__(self, self._text_frame, frame_info)
938        self._firstlineno = frame_info.firstlineno
939
940        self._load_code(frame_info)
941        self._text_frame.text.focus()
942        self.update()
943
944    def _init_layout_widgets(self, master, frame_info):
945        self.main_frame = ttk.Frame(
946            self
947        )  # just a background behind padding of main_pw, without this OS X leaves white border
948        self.main_frame.grid(sticky=tk.NSEW)
949        self.rowconfigure(0, weight=1)
950        self.columnconfigure(0, weight=1)
951        self.main_pw = ui_utils.AutomaticPanedWindow(self.main_frame, orient=tk.VERTICAL)
952        self.main_pw.grid(sticky=tk.NSEW, padx=10, pady=10)
953        self.main_frame.rowconfigure(0, weight=1)
954        self.main_frame.columnconfigure(0, weight=1)
955
956        self._code_book = ttk.Notebook(self.main_pw)
957        self._text_frame = CodeView(
958            self._code_book, first_line_number=frame_info.firstlineno, font="EditorFont"
959        )
960        self._code_book.add(self._text_frame, text=tr("Source code"))
961        self.main_pw.add(self._code_book, minsize=200)
962        self._code_book.preferred_size_in_pw = 400
963
964    def _load_code(self, frame_info):
965        self._text_frame.set_content(frame_info.source)
966
967    def _update_this_frame(self, msg, frame_info):
968        FrameVisualizer._update_this_frame(self, msg, frame_info)
969
970    def bring_out_this_frame(self):
971        self.focus_set()  # no effect when clicking on stack view
972        var_view = get_workbench().get_view("VariablesView")
973        var_view.show_globals(self._frame_info.globals, self._frame_info.module_name)
974
975    def _on_focus(self, event):
976        # TODO: bring out main frame when main window gets focus
977        self.bring_out_this_frame()
978
979    def _on_close(self):
980        showinfo(
981            tr("Can't close yet"),
982            tr('Use "Stop" command if you want to cancel debugging'),
983            master=self,
984        )
985
986    def close(self, frame_id=None):
987        super().close()
988
989        if frame_id is None or frame_id == self._frame_id:
990            self.destroy()
991
992
993class FunctionCallDialog(DialogVisualizer):
994    def __init__(self, master, frame_info):
995        DialogVisualizer.__init__(self, master, frame_info)
996
997    def _init_layout_widgets(self, master, frame_info):
998        DialogVisualizer._init_layout_widgets(self, master, frame_info)
999        self._locals_book = ttk.Notebook(self.main_pw)
1000        self._locals_frame = VariablesFrame(self._locals_book)
1001        self._locals_book.preferred_size_in_pw = 200
1002        self._locals_book.add(self._locals_frame, text=tr("Local variables"))
1003        self.main_pw.add(self._locals_book, minsize=100)
1004
1005    def _load_code(self, frame_info):
1006        DialogVisualizer._load_code(self, frame_info)
1007
1008        function_label = frame_info.code_name
1009
1010        # change tab label
1011        self._code_book.tab(self._text_frame, text=function_label)
1012
1013    def _update_this_frame(self, msg, frame_info):
1014        DialogVisualizer._update_this_frame(self, msg, frame_info)
1015        self._locals_frame.update_variables(frame_info.locals)
1016
1017
1018class ModuleLoadDialog(DialogVisualizer):
1019    def __init__(self, text_frame, frame_info):
1020        DialogVisualizer.__init__(self, text_frame, frame_info)
1021
1022
1023class StackView(ui_utils.TreeFrame):
1024    def __init__(self, master):
1025        super().__init__(
1026            master, ("function", "location", "id"), displaycolumns=("function", "location")
1027        )
1028
1029        # self.tree.configure(show="tree")
1030        self.tree.column("#0", width=0, anchor=tk.W, stretch=False)
1031        self.tree.column("function", width=120, anchor=tk.W, stretch=False)
1032        self.tree.column("location", width=450, anchor=tk.W, stretch=True)
1033
1034        self.tree.heading("function", text=tr("Function"), anchor=tk.W)
1035        self.tree.heading("location", text=tr("Location"), anchor=tk.W)
1036
1037        get_workbench().bind("DebuggerResponse", self._update_stack, True)
1038        get_workbench().bind("ToplevelResponse", lambda e=None: self._clear_tree(), True)
1039        get_workbench().bind("debugger_return_response", self._handle_debugger_return, True)
1040
1041    def _update_stack(self, msg):
1042        self._clear_tree()
1043
1044        node_id = None
1045        for frame in msg.stack:
1046            lineno = frame.focus.lineno
1047
1048            node_id = self.tree.insert("", "end")
1049            self.tree.set(node_id, "function", frame.code_name)
1050            self.tree.set(
1051                node_id, "location", "%s, line %s" % (os.path.basename(frame.filename), lineno)
1052            )
1053            self.tree.set(node_id, "id", frame.id)
1054
1055        # select last frame
1056        if node_id is not None:
1057            self.tree.see(node_id)
1058            self.tree.selection_add(node_id)
1059            self.tree.focus(node_id)
1060
1061    def _handle_debugger_return(self, msg):
1062        delete = False
1063        for iid in self.tree.get_children():
1064            if self.tree.set(iid, "id") == msg["frame_id"]:
1065                # start deleting from this frame
1066                delete = True
1067
1068            if delete:
1069                self.tree.delete(iid)
1070
1071    def on_select(self, event):
1072        iid = self.tree.focus()
1073        if iid != "":
1074            # assuming id is in the last column
1075            frame_id = self.tree.item(iid)["values"][-1]
1076            if _current_debugger is not None:
1077                _current_debugger.bring_out_frame(frame_id)
1078
1079
1080class ExceptionView(TextFrame):
1081    def __init__(self, master):
1082        super().__init__(
1083            master,
1084            borderwidth=0,
1085            relief="solid",
1086            undo=False,
1087            read_only=True,
1088            font="TkDefaultFont",
1089            text_class=SyntaxText,
1090            foreground=get_syntax_options_for_tag("stderr")["foreground"],
1091            highlightthickness=0,
1092            padx=5,
1093            pady=5,
1094            wrap="char",
1095            horizontal_scrollbar=False,
1096        )
1097
1098        self.text.tag_configure("hyperlink", **get_syntax_options_for_tag("hyperlink"))
1099        self.text.tag_bind("hyperlink", "<Enter>", self._hyperlink_enter)
1100        self.text.tag_bind("hyperlink", "<Leave>", self._hyperlink_leave)
1101        get_workbench().bind("ToplevelResponse", self._on_toplevel_response, True)
1102
1103        self._prev_exception = None
1104
1105        self._show_description()
1106
1107    def _show_description(self):
1108        self.text.configure(foreground=get_syntax_options_for_tag("TEXT")["foreground"])
1109        self.text.direct_insert(
1110            "end",
1111            tr("If last command raised an exception then this view will show the stacktrace."),
1112        )
1113
1114    def set_exception(self, exception_lines_with_frame_info):
1115        if exception_lines_with_frame_info == self._prev_exception:
1116            return
1117
1118        self.text.direct_delete("1.0", "end")
1119
1120        if exception_lines_with_frame_info is None:
1121            self._show_description()
1122            return
1123
1124        self.text.configure(foreground=get_syntax_options_for_tag("stderr")["foreground"])
1125        for line, frame_id, filename, lineno in exception_lines_with_frame_info:
1126
1127            if frame_id is not None:
1128                frame_tag = "frame_%d" % frame_id
1129
1130                def handle_frame_click(event, frame_id=frame_id, filename=filename, lineno=lineno):
1131                    get_runner().send_command(InlineCommand("get_frame_info", frame_id=frame_id))
1132                    if os.path.exists(filename):
1133                        get_workbench().get_editor_notebook().show_file(
1134                            filename, lineno, set_focus=False
1135                        )
1136
1137                self.text.tag_bind(frame_tag, "<1>", handle_frame_click, True)
1138
1139                start = max(line.find("File"), 0)
1140                end = line.replace("\r", "").find("\n")
1141                if end < 10:
1142                    end = len(line)
1143
1144                self.text.direct_insert("end", line[:start])
1145                self.text.direct_insert("end", line[start:end], ("hyperlink", frame_tag))
1146                self.text.direct_insert("end", line[end:])
1147
1148            else:
1149                self.text.direct_insert("end", line)
1150
1151        self._prev_exception = exception_lines_with_frame_info
1152
1153    def _on_toplevel_response(self, msg):
1154        if "user_exception" in msg:
1155            self.set_exception(msg["user_exception"]["items"])
1156        else:
1157            self.set_exception(None)
1158
1159    def _hyperlink_enter(self, event):
1160        self.text.config(cursor="hand2")
1161
1162    def _hyperlink_leave(self, event):
1163        self.text.config(cursor="")
1164
1165
1166def _debugger_command_enabled(command):
1167    if _current_debugger is None:
1168        return False
1169    else:
1170        return _current_debugger.command_enabled(command)
1171
1172
1173def _issue_debugger_command(command):
1174    if _current_debugger is None:
1175        raise AssertionError("Trying to send debugger command without debugger")
1176    else:
1177        return _current_debugger.check_issue_command(command)
1178
1179
1180def _start_debug_enabled():
1181    return (
1182        _current_debugger is None
1183        and get_workbench().get_editor_notebook().get_current_editor() is not None
1184        and "debug" in get_runner().get_supported_features()
1185    )
1186
1187
1188def _request_debug(command_name):
1189    # Don't assume Debug command gets issued after this
1190    # This may just call the %cd command
1191    # or the user may deny saving current editor
1192    if get_workbench().in_simple_mode():
1193        get_workbench().show_view("VariablesView")
1194
1195    get_runner().execute_current(command_name)
1196
1197
1198def _debug_accepted(event):
1199    # Called when proxy accepted the debug command
1200    global _current_debugger
1201    cmd = event.command
1202    if cmd.get("name") in ["Debug", "FastDebug"]:
1203        assert _current_debugger is None
1204        if get_workbench().get_option("debugger.frames_in_separate_windows"):
1205            _current_debugger = StackedWindowsDebugger()
1206        else:
1207            _current_debugger = SingleWindowDebugger()
1208
1209
1210def _handle_debugger_progress(msg):
1211    global _current_debugger
1212    assert _current_debugger is not None
1213    _current_debugger.handle_debugger_progress(msg)
1214    _update_run_or_resume_button()
1215
1216
1217def _handle_toplevel_response(msg):
1218    global _current_debugger
1219    if _current_debugger is not None:
1220        _current_debugger.close()
1221        _current_debugger = None
1222
1223    _update_run_or_resume_button()
1224
1225
1226def _handle_debugger_return(msg):
1227    global _current_debugger
1228    assert _current_debugger is not None
1229    _current_debugger.handle_debugger_return(msg)
1230
1231
1232def _run_or_resume():
1233    state = get_runner().get_state()
1234    if state == "waiting_debugger_command":
1235        _issue_debugger_command("resume")
1236    elif state == "waiting_toplevel_command":
1237        get_runner().cmd_run_current_script()
1238
1239
1240def _run_or_resume_enabled():
1241    return get_runner().cmd_run_current_script_enabled() or _debugger_command_enabled("resume")
1242
1243
1244def _update_run_or_resume_button():
1245    if not get_workbench().in_simple_mode():
1246        return
1247
1248    state = get_runner().get_state()
1249    if state == "waiting_debugger_command":
1250        caption = RESUME_COMMAND_CAPTION
1251        image = get_workbench().get_image("resume")
1252    elif state == "waiting_toplevel_command":
1253        caption = running.RUN_COMMAND_CAPTION
1254        image = get_workbench().get_image("run-current-script")
1255    else:
1256        return
1257
1258    button = get_workbench().get_toolbar_button("runresume")
1259    button.configure(text=caption, image=image)
1260
1261
1262def get_current_debugger():
1263    return _current_debugger
1264
1265
1266def run_preferred_debug_command():
1267    preferred_debugger = get_workbench().get_option("debugger.preferred_debugger").lower()
1268    if preferred_debugger == "faster":
1269        return _request_debug("FastDebug")
1270    elif preferred_debugger == "birdseye":
1271        from thonny.plugins.birdseye_frontend import debug_with_birdseye
1272
1273        return debug_with_birdseye()
1274    else:
1275        return _request_debug("Debug")
1276
1277
1278def load_plugin() -> None:
1279
1280    global RESUME_COMMAND_CAPTION
1281    RESUME_COMMAND_CAPTION = tr("Resume")
1282
1283    if get_workbench().in_simple_mode():
1284        get_workbench().set_default("debugger.frames_in_separate_windows", False)
1285    else:
1286        get_workbench().set_default("debugger.frames_in_separate_windows", True)
1287
1288    get_workbench().set_default("debugger.automatic_stack_view", True)
1289    get_workbench().set_default(
1290        "debugger.preferred_debugger", "faster" if running_on_rpi() else "nicer"
1291    )
1292    get_workbench().set_default("debugger.allow_stepping_into_libraries", False)
1293
1294    get_workbench().add_command(
1295        "runresume",
1296        "run",
1297        tr("Run / resume"),
1298        _run_or_resume,
1299        caption=running.RUN_COMMAND_CAPTION,
1300        tester=_run_or_resume_enabled,
1301        default_sequence=None,
1302        group=10,
1303        image="run-current-script",
1304        include_in_menu=False,
1305        include_in_toolbar=get_workbench().in_simple_mode(),
1306        alternative_caption=RESUME_COMMAND_CAPTION,
1307    )
1308
1309    get_workbench().add_command(
1310        "debug_preferred",
1311        "run",
1312        tr("Debug current script"),
1313        run_preferred_debug_command,
1314        caption=tr("Debug"),
1315        tester=_start_debug_enabled,
1316        group=10,
1317        image="debug-current-script",
1318        include_in_menu=False,
1319        include_in_toolbar=True,
1320    )
1321
1322    get_workbench().add_command(
1323        "debug_nicer",
1324        "run",
1325        tr("Debug current script (nicer)"),
1326        lambda: _request_debug("Debug"),
1327        caption="Debug (nicer)",
1328        tester=_start_debug_enabled,
1329        default_sequence="<Control-F5>",
1330        group=10,
1331        # image="debug-current-script",
1332    )
1333
1334    get_workbench().add_command(
1335        "debug_faster",
1336        "run",
1337        tr("Debug current script (faster)"),
1338        lambda: _request_debug("FastDebug"),
1339        caption="Debug (faster)",
1340        tester=_start_debug_enabled,
1341        default_sequence="<Shift-F5>",
1342        group=10,
1343    )
1344
1345    get_workbench().add_command(
1346        "step_over",
1347        "run",
1348        tr("Step over"),
1349        lambda: _issue_debugger_command("step_over"),
1350        caption=tr("Over"),
1351        tester=lambda: _debugger_command_enabled("step_over"),
1352        default_sequence="<F6>",
1353        group=30,
1354        image="step-over",
1355        include_in_toolbar=True,
1356    )
1357
1358    get_workbench().add_command(
1359        "step_into",
1360        "run",
1361        tr("Step into"),
1362        lambda: _issue_debugger_command("step_into"),
1363        caption=tr("Into"),
1364        tester=lambda: _debugger_command_enabled("step_into"),
1365        default_sequence="<F7>",
1366        group=30,
1367        image="step-into",
1368        include_in_toolbar=True,
1369    )
1370
1371    get_workbench().add_command(
1372        "step_out",
1373        "run",
1374        tr("Step out"),
1375        lambda: _issue_debugger_command("step_out"),
1376        caption=tr("Out"),
1377        tester=lambda: _debugger_command_enabled("step_out"),
1378        group=30,
1379        image="step-out",
1380        include_in_toolbar=True,
1381    )
1382
1383    get_workbench().add_command(
1384        "resume",
1385        "run",
1386        RESUME_COMMAND_CAPTION,
1387        lambda: _issue_debugger_command("resume"),
1388        caption=RESUME_COMMAND_CAPTION,
1389        tester=lambda: _debugger_command_enabled("resume"),
1390        default_sequence="<F8>",
1391        group=30,
1392        image="resume",
1393        include_in_toolbar=not get_workbench().in_simple_mode(),
1394    )
1395
1396    get_workbench().add_command(
1397        "run_to_cursor",
1398        "run",
1399        tr("Run to cursor"),
1400        lambda: _issue_debugger_command("run_to_cursor"),
1401        tester=lambda: _debugger_command_enabled("run_to_cursor"),
1402        default_sequence=select_sequence("<Control-F8>", "<Control-F8>"),
1403        group=30,
1404        image="run-to-cursor",
1405        include_in_toolbar=False,
1406    )
1407
1408    get_workbench().add_command(
1409        "step_back",
1410        "run",
1411        tr("Step back"),
1412        lambda: _issue_debugger_command("step_back"),
1413        caption=tr("Back"),
1414        tester=lambda: _debugger_command_enabled("step_back"),
1415        default_sequence=select_sequence("<Control-b>", "<Command-b>"),
1416        group=30,
1417    )
1418
1419    get_workbench().add_view(StackView, tr("Stack"), "se")
1420    get_workbench().add_view(ExceptionView, tr("Exception"), "s")
1421    get_workbench().bind("DebuggerResponse", _handle_debugger_progress, True)
1422    get_workbench().bind("ToplevelResponse", _handle_toplevel_response, True)
1423    get_workbench().bind("debugger_return_response", _handle_debugger_return, True)
1424    get_workbench().bind("CommandAccepted", _debug_accepted, True)
1425