Skip to content

Commit

Permalink
feat: Code debugging support (built-in python debugger) #292
Browse files Browse the repository at this point in the history
 from tomlin7/debugger
  • Loading branch information
tomlin7 authored Apr 21, 2024
2 parents 21b5742 + ac3b865 commit afc95e9
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 110 deletions.
1 change: 1 addition & 0 deletions biscuit/core/components/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Components
Contains all the components of editor (Editor, Games, Git, LSP, Extensions Manager, Views, Menus and Palette)
"""
from .debugger import DebuggerInfo, PythonDebugger, get_debugger
from .editors import BaseEditor, Editor
from .floating import *
from .games import BaseGame, register_game
Expand Down
16 changes: 16 additions & 0 deletions biscuit/core/components/debugger/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from __future__ import annotations

import typing

if typing.TYPE_CHECKING:
from biscuit.core.components.editors import TextEditor

from .output import DebuggerInfo
from .python_dbd import PythonDebugger


def get_debugger(editor: TextEditor):
if editor.text.language == "Python":
return PythonDebugger(editor)

# others are not supported yet
46 changes: 46 additions & 0 deletions biscuit/core/components/debugger/output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from biscuit.core.utils import Text, Toplevel


class DebuggerInfo(Toplevel):
"""A Toplevel window that displays information about the debugger."""

def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.title("Debugger Information")
self.text_widget = Text(self, wrap='word', width=60, height=50)
self.text_widget.config(font=self.base.settings.font, **self.base.theme.editors.text)
self.text_widget.pack(fill='both', expand=True)
self.withdraw()

self.wm_protocol("WM_DELETE_WINDOW", self.withdraw)

def clear(self):
"""Clear the output."""

try:
self.text_widget.delete('1.0', 'end')
except:
pass

def show_variables(self, frame):
"""Show the local variables in the given frame.
Args:
frame (frame): The frame to show the local variables of."""

locals_str = "Local Variables:\n"
for var, val in frame.f_locals.items():
locals_str += f"{var}: {val}\n"
self.text_widget.insert('end', locals_str)

def show_callstack(self, frame):
"""Show the call stack from the given frame.
Args:
frame (frame): The frame to show the call stack from."""

callstack_str = "\nCall Stack:\n"
while frame:
callstack_str += f"{frame.f_code.co_name} at {frame.f_code.co_filename}, line {frame.f_lineno}\n"
frame = frame.f_back
self.text_widget.insert('end', callstack_str)
47 changes: 47 additions & 0 deletions biscuit/core/components/debugger/python_dbd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from __future__ import annotations

import bdb
import typing

if typing.TYPE_CHECKING:
from biscuit.core.components.editors import TextEditor


class PythonDebugger(bdb.Bdb):
"""Python debugger class.
This class is used to debug Python code. It uses python's standard bdb and
overrides some of its methods to provide custom functionality. The debugger
information is displayed in a DebuggerInfo window.
Args:
editor: The TextEditor instance that is being debugged."""

def __init__(self, editor: TextEditor):
super().__init__()
self.editor = self.master = editor
self.base = editor.base
self.file_path = self.editor.path
self.output = self.base.debugger_info

def run(self, *_):
"""Debug the code in the editor."""
self.reset()

with open(self.file_path, 'r') as f:
code = compile(f.read(), self.file_path, 'exec')

self.clear_all_breaks()
for line_number in self.editor.breakpoints:
self.set_break(self.file_path, line_number)

self.set_trace()
exec(code, globals(), locals())

def user_line(self, frame):
self.output.clear()
self.output.show_variables(frame)
self.output.show_callstack(frame)
self.output.deiconify()

self.set_continue()
15 changes: 13 additions & 2 deletions biscuit/core/components/editors/texteditor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from hashlib import md5
from tkinter.font import Font

from biscuit.core.components.debugger import get_debugger
from biscuit.core.utils import Scrollbar

from ..comment_prefix import register_comment_prefix
Expand Down Expand Up @@ -49,7 +50,7 @@ def __init__(self, master, path=None, exists=True, language=None, minimalist=Fal

if not self.standalone:
self.run_command_value = self.base.exec_manager.get_command(self)
self.__buttons__.insert(0, ('run', self.run_file))
self.__buttons__.insert(0, ('run', lambda: self.run_file()))

self.runmenu = RunMenu(self, "run menu")
if self.run_command_value:
Expand All @@ -61,7 +62,13 @@ def __init__(self, master, path=None, exists=True, language=None, minimalist=Fal
self.runmenu.add_command("Configure Run...", lambda: self.base.commands.show_run_config_palette(self.run_command_value))

self.__buttons__.insert(1, ('chevron-down', self.runmenu.show))


self.debugger = get_debugger(self)
if self.debugger:
self.__buttons__.insert(2, ('bug', self.debugger.run))
self.runmenu.add_separator()
self.runmenu.add_command(f"Debug {self.language} file", self.debugger.run)

self.linenumbers.attach(self.text)
if not self.minimalist:
self.minimap.attach(self.text)
Expand Down Expand Up @@ -99,6 +106,10 @@ def calculate_content_hash(self):
text = self.text.get_all_text()
return md5(text.encode()).hexdigest()

@property
def breakpoints(self):
return self.linenumbers.breakpoints

@property
def unsaved_changes(self):
""" Check if the editor content has changed """
Expand Down
78 changes: 78 additions & 0 deletions biscuit/core/components/editors/texteditor/linenumbers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import tkinter as tk

from biscuit.core.utils import Canvas, Menubutton


class LineNumbers(Canvas):
def __init__(self, master, text=None, font=None, *args, **kwargs) -> None:
super().__init__(master, *args, **kwargs)
self.font = font
self.config(width=65, bd=0, highlightthickness=0, **self.base.theme.editors.linenumbers)
self.text = text

self.bg, self.fg, _, self.hfg = self.base.theme.editors.linenumbers.number.values()
self.bp_hover_color, _, self.bp_enabled_color, _ = self.base.theme.editors.linenumbers.breakpoint.values()
self.breakpoints = set()

def attach(self, text):
self.text = text

def mark_line(self, line):
dline = self.text.dlineinfo(line)

if not dline:
return

y = dline[1]
btn = Menubutton(self,
text=">", font=self.font, cursor="hand2", borderwidth=0,
width=2, height=1, pady=0, padx=0, relief=tk.FLAT, **self.base.theme.linenumbers)
self.create_window(70, y-2, anchor=tk.NE, window=btn)

def set_bar_width(self, width):
self.configure(width=width)

def toggle_breakpoint(self, line):
if line in self.breakpoints:
self.breakpoints.remove(line)
else:
self.breakpoints.add(line)
self.redraw()

def redraw(self, *_):
self.delete(tk.ALL)

if not self.text:
return

i = self.text.index("@0,0")
while True:
dline = self.text.dlineinfo(i)
if dline is None:
break

y = dline[1]
linenum = int(float(i))
curline = self.text.dlineinfo(tk.INSERT)
cur_y = curline[1] if curline else None

# Render breakpoint
has_breakpoint = linenum in self.breakpoints
breakpoint_id = self.create_oval(5, y + 3, 15, y + 13, outline="", fill=self.bg if not has_breakpoint else self.bp_enabled_color)

# Bind hover and click events for breakpoint
self.tag_bind(breakpoint_id, "<Enter>",
lambda _, breakpoint_id=breakpoint_id, flag=has_breakpoint: self.on_breakpoint_enter(breakpoint_id, flag))
self.tag_bind(breakpoint_id, "<Leave>",
lambda _, breakpoint_id=breakpoint_id, flag=has_breakpoint: self.on_breakpoint_leave(breakpoint_id, flag))
self.tag_bind(breakpoint_id, "<Button-1>",
lambda _, linenum=linenum: self.toggle_breakpoint(linenum))

self.create_text(40, y, anchor=tk.NE, text=linenum, font=self.font, tag=i, fill=self.hfg if y == cur_y else self.fg)
i = self.text.index(f"{i}+1line")

def on_breakpoint_enter(self, id, flag):
self.itemconfig(id, fill=self.bp_enabled_color if flag else self.bp_hover_color)

def on_breakpoint_leave(self, id, flag):
self.itemconfig(id, fill=self.bp_enabled_color if flag else self.bg)
63 changes: 0 additions & 63 deletions biscuit/core/components/editors/texteditor/linenumbers/__init__.py

This file was deleted.

This file was deleted.

1 change: 1 addition & 0 deletions biscuit/core/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def setup_floating_widgets(self) -> None:
self.rename = Rename(self)
self.pathview = PathView(self)
self.hover = Hover(self)
self.debugger_info = DebuggerInfo(self)

def late_setup(self) -> None:
# for components that are dependant on other components (called after initialization)
Expand Down
5 changes: 4 additions & 1 deletion biscuit/core/settings/config/theme/dark.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sv_ttk
from pygments.token import Token

from .theme import Theme
from .theme import *


class Dark(Theme):
Expand Down Expand Up @@ -52,3 +52,6 @@ def __init__(self, *args, **kwds) -> None:
self.views.panel.logs.info = "#b5cea8"
self.views.panel.logs.warning = "#ce9178"
self.views.panel.logs.error = "#ce9178"

self.editors.linenumbers.breakpoint.background = '#6e1b13'
self.editors.linenumbers.breakpoint.highlightbackground = '#e51400'
6 changes: 6 additions & 0 deletions biscuit/core/settings/config/theme/theme.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from collections.abc import Mapping

from pygments.token import Token
Expand Down Expand Up @@ -26,6 +28,9 @@ def __init__(self, master, background: str=None, foreground: str=None,
setattr(self, key, value)

def values(self):
"""Returns a tuple of color values, order:
Background, Foreground, HighlightBackground, HighlightForeground"""

return self.background, self.foreground, self.highlightbackground, self.highlightforeground

def to_dict(self):
Expand Down Expand Up @@ -217,6 +222,7 @@ def __init__(self, *args, **kwargs) -> None:
self.linenumbers.number = ThemeObject(self.linenumbers)
self.linenumbers.number.foreground = "#6e7681"
self.linenumbers.number.highlightforeground = "#171184"
self.linenumbers.breakpoint = HighlightableThemeObject(self.linenumbers, '#f5a199', 'white', '#e51400', 'white')

self.autocomplete = FrameThemeObject(self)
self.autocomplete.item = ThemeObject(self.autocomplete)
Expand Down
Loading

0 comments on commit afc95e9

Please sign in to comment.