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