Compare commits

...

6 Commits

Author SHA1 Message Date
060ab01648 Add simple variables viewer, that doesnt cut of line 2026-03-29 18:46:31 +02:00
bb829286d1 Add removing of breakpoints 2026-03-29 17:53:34 +02:00
436a49b1b3 Working codeviewer implemented 2026-03-29 17:15:20 +02:00
387804dbd0 working codeviewer 2026-03-28 19:24:17 +01:00
d254d5cfd0 Only show locals and remove globals from list 2026-03-28 18:27:59 +01:00
7e02e8ec6d Fix module importing 2026-03-28 18:23:50 +01:00
10 changed files with 277 additions and 87 deletions

View File

@@ -8,6 +8,7 @@ import (
"io" "io"
"log/slog" "log/slog"
"os/exec" "os/exec"
"slices"
"sync" "sync"
) )
@@ -159,13 +160,19 @@ func (b *Bridge) Step() error {
return nil return nil
} }
func (b *Bridge) Breakpoint(file string, line int) error { func (b *Bridge) Breakpoint(file string, line int) (set bool, err error) {
// Check if breakpoint already exists here
if !b.running { if !b.running {
return ErrNotRunning return false, ErrNotRunning
} }
requestId, cmd := makeCommand(BreakCommand, map[string]any{ var command CommandType
if _, ok := b.breakpoints[file]; ok && slices.Contains(b.breakpoints[file], line) {
command = UnbreakCommand
} else {
command = BreakCommand
}
requestId, cmd := makeCommand(command, map[string]any{
"file": file, "file": file,
"line": line, "line": line,
}) })
@@ -175,18 +182,26 @@ func (b *Bridge) Breakpoint(file string, line int) error {
obj := <-c obj := <-c
var m map[string]any var m map[string]any
err := json.Unmarshal([]byte(obj), &m) err = json.Unmarshal([]byte(obj), &m)
if err != nil { if err != nil {
return err return false, err
} }
if m["status"] != "ok" { if m["status"] != "ok" {
return fmt.Errorf("error occured on break, err: %s", m["error"]) return false, fmt.Errorf("error occured on break, err: %s", m["error"])
} }
b.breakpoints[file] = append(b.breakpoints[file], line) if command == BreakCommand {
b.breakpoints[file] = append(b.breakpoints[file], line)
return true, nil
} else {
breakpointsLen := len(b.breakpoints[file])
index := slices.Index(b.breakpoints[file], line)
b.breakpoints[file][index] = b.breakpoints[file][breakpointsLen-1]
return nil b.breakpoints[file] = b.breakpoints[file][0 : breakpointsLen-1]
return false, nil
}
} }
func (b *Bridge) Continue() error { func (b *Bridge) Continue() error {

View File

@@ -11,6 +11,7 @@ type CommandType string
const ( const (
ContinueCommand CommandType = "continue" ContinueCommand CommandType = "continue"
BreakCommand CommandType = "break" BreakCommand CommandType = "break"
UnbreakCommand CommandType = "unbreak"
LocalsCommand CommandType = "locals" LocalsCommand CommandType = "locals"
StepCommand CommandType = "step" StepCommand CommandType = "step"
) )

View File

@@ -17,7 +17,7 @@ class PyBugBridgeDebugger(bdb.Bdb):
return json.loads(sys.stdin.readline()) return json.loads(sys.stdin.readline())
def user_line(self, frame: FrameType): def user_line(self, frame: FrameType):
# print("TRACE:", frame.f_code.co_filename, frame.f_lineno) print("TRACE:", frame.f_code.co_filename, frame.f_lineno)
self.send( self.send(
{ {
"event": "stopped", "event": "stopped",
@@ -30,7 +30,9 @@ class PyBugBridgeDebugger(bdb.Bdb):
def interaction_loop(self, frame: FrameType): def interaction_loop(self, frame: FrameType):
while True: while True:
print("DEBUG: waiting for command", file=sys.stderr, flush=True)
cmd = self.recv() cmd = self.recv()
print(f"DEBUG: received command {cmd}", file=sys.stderr, flush=True)
match cmd["cmd"]: match cmd["cmd"]:
case "continue": case "continue":
@@ -44,7 +46,11 @@ class PyBugBridgeDebugger(bdb.Bdb):
{ {
"request_id": cmd["request_id"], "request_id": cmd["request_id"],
"event": "locals", "event": "locals",
"vars": {k: repr(v) for k, v in frame.f_locals.items()}, "vars": {
k: repr(v)
for k, v in frame.f_locals.items()
if k not in frame.f_globals.keys()
},
} }
) )
case "eval": case "eval":
@@ -86,6 +92,25 @@ class PyBugBridgeDebugger(bdb.Bdb):
"status": "ok", "status": "ok",
} }
) )
case "unbreak":
err = self.clear_break(cmd["file"], cmd["line"])
if err:
self.send(
{
"request_id": cmd["request_id"],
"event": "unbreak",
"status": "error",
"error": err,
}
)
else:
self.send(
{
"request_id": cmd["request_id"],
"event": "unbreak",
"status": "ok",
}
)
def main(): def main():
@@ -107,7 +132,7 @@ def main():
} }
dbg.set_step() dbg.set_step()
dbg.run(code, globals_dict, {}) dbg.run(code, globals_dict, globals_dict)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -64,7 +64,6 @@ def generate_company(name: str, num_employees: int) -> Company:
def main(): def main():
my_company = generate_company("TechCorp", 5) my_company = generate_company("TechCorp", 5)
print("All employees:") print("All employees:")
for emp in my_company.employees: for emp in my_company.employees:
print(emp) print(emp)

14
ui/codeviewer/messages.go Normal file
View File

@@ -0,0 +1,14 @@
package codeviewer
type ChangeTextMsg struct {
Text string
}
type BreakpointMsg struct {
Line int
Added bool
}
type ExecutionStoppedMsg struct {
Line int
}

124
ui/codeviewer/model.go Normal file
View File

@@ -0,0 +1,124 @@
package codeviewer
import (
"bytes"
"fmt"
"strings"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
tea "github.com/charmbracelet/bubbletea"
)
type CodeViewer struct {
lines []string
Width, Height int
Cursor int
offset int
breakpoints map[int]struct{}
currStoppedLine int
}
func NewCodeViewer(text string) CodeViewer {
cv := CodeViewer{
Width: 0,
Height: 0,
Cursor: 0,
offset: 0,
currStoppedLine: -1,
breakpoints: map[int]struct{}{},
}
return cv.colorize(text)
}
func (c CodeViewer) Init() tea.Cmd {
return nil
}
func (c CodeViewer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return c.handleKeyMsg(msg)
case ChangeTextMsg:
c = c.colorize(msg.Text)
case BreakpointMsg:
if msg.Added {
c.breakpoints[msg.Line] = struct{}{}
} else {
delete(c.breakpoints, msg.Line)
}
case ExecutionStoppedMsg:
c.currStoppedLine = msg.Line
}
return c, nil
}
func (c CodeViewer) handleKeyMsg(key tea.KeyMsg) (tea.Model, tea.Cmd) {
switch key.String() {
case "k":
c.Cursor = max(0, c.Cursor-1)
topThreshold := c.offset + int(float64(c.Height)*0.1)
if c.Cursor < topThreshold && c.offset > 0 {
c.offset -= 1
}
case "j":
c.Cursor = min(len(c.lines)-1, c.Cursor+1)
bottomThreshold := c.offset + int(float64(c.Height)*0.9)
if c.Cursor > bottomThreshold && c.offset < len(c.lines) {
c.offset += 1
}
}
return c, nil
}
func (c CodeViewer) colorize(text string) CodeViewer {
var buf bytes.Buffer
lexer := lexers.Get("python")
style := styles.Get("monokai")
formatter := formatters.Get("terminal16m")
iterator, _ := lexer.Tokenise(nil, text)
formatter.Format(&buf, style, iterator)
c.lines = strings.Split(buf.String(), "\n")
return c
}
func (c CodeViewer) View() string {
var out strings.Builder
lines := c.lines[max(0, c.offset):min(c.offset+c.Height, len(c.lines))]
for i, line := range lines {
lineNumber := i + c.offset + 1
breakpoint := " "
cursor := " "
executor := " "
if lineNumber == c.Cursor+1 {
cursor = ">"
}
if _, ok := c.breakpoints[lineNumber]; ok {
breakpoint = "O"
}
if lineNumber == c.currStoppedLine {
executor = "@"
}
fmt.Fprintf(&out, "%-4d%s%s%s %s\n", lineNumber, breakpoint, executor, cursor, line)
}
// Count long lines, and add here if they are out of view, this is a bit tricky I guess
if len(lines)-c.offset < c.Height {
for range c.Height - len(lines) {
fmt.Fprintf(&out, "\n")
}
}
return out.String()
}

View File

@@ -1,17 +1,15 @@
package ui package ui
import ( import (
"strings"
"git.pablu.de/pablu/pybug/internal/bridge" "git.pablu.de/pablu/pybug/internal/bridge"
"git.pablu.de/pablu/pybug/ui/codeviewer"
"git.pablu.de/pablu/pybug/ui/variablesviewer"
"github.com/charmbracelet/bubbles/viewport" "github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
type Model struct { type Model struct {
currentFile string currentFile string
text string
textLines int
width int width int
height int height int
@@ -20,14 +18,12 @@ type Model struct {
listenBridge chan string listenBridge chan string
listenBridgeExecutionsStopped chan bridge.ExecutionPoint listenBridgeExecutionsStopped chan bridge.ExecutionPoint
currLocals map[string]any currLocals map[string]any
currExecutionPoint bridge.ExecutionPoint
messages []string messages []string
stdoutOutput viewport.Model stdoutOutput viewport.Model
codeViewer viewport.Model codeViewer codeviewer.CodeViewer
localsViewer viewport.Model localsViewer variablesviewer.VariableViewer
cursor int
breakpoints map[string][]int breakpoints map[string][]int
} }
@@ -37,27 +33,20 @@ func NewModel(b *bridge.Bridge, file string, text string) Model {
c2 := b.SubscribeStopped() c2 := b.SubscribeStopped()
stdoutOutput := viewport.New(0, 0) stdoutOutput := viewport.New(0, 0)
codeViewer := viewport.New(0, 0) codeViewer := codeviewer.NewCodeViewer(text)
localsViewer := viewport.New(0, 0) localsViewer := variablesviewer.NewVariableViewer(map[string]any{})
return Model{ return Model{
currentFile: file, currentFile: file,
text: text,
textLines: len(strings.Split(text, "\n")),
bridge: b, bridge: b,
listenBridge: c, listenBridge: c,
listenBridgeExecutionsStopped: c2, listenBridgeExecutionsStopped: c2,
currLocals: make(map[string]any), currLocals: make(map[string]any),
currExecutionPoint: bridge.ExecutionPoint{
File: file,
Line: 0,
},
breakpoints: make(map[string][]int), breakpoints: make(map[string][]int),
messages: make([]string, 0), messages: make([]string, 0),
cursor: 0,
codeViewer: codeViewer, codeViewer: codeViewer,
stdoutOutput: stdoutOutput, stdoutOutput: stdoutOutput,
localsViewer: localsViewer, localsViewer: localsViewer,
@@ -74,12 +63,14 @@ func ListenBridge(ch <-chan string) tea.Cmd {
func ListenBridgeExecutionsStopped(ch <-chan bridge.ExecutionPoint) tea.Cmd { func ListenBridgeExecutionsStopped(ch <-chan bridge.ExecutionPoint) tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
msg := <-ch msg := <-ch
return ExecutionStoppedMsg(msg) return codeviewer.ExecutionStoppedMsg{
Line: msg.Line,
// TODO, WE IGNORE FILE HERE
}
} }
} }
type LocalsMsg map[string]any type LocalsMsg map[string]any
type ExecutionStoppedMsg bridge.ExecutionPoint
type StdoutMsg string type StdoutMsg string
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {

View File

@@ -4,11 +4,14 @@ import (
"log/slog" "log/slog"
"strings" "strings"
"git.pablu.de/pablu/pybug/internal/bridge" "git.pablu.de/pablu/pybug/ui/codeviewer"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
updatedCv, cmd := m.codeViewer.Update(msg)
m.codeViewer = updatedCv.(codeviewer.CodeViewer)
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
return m.HandleKeyMsg(msg) return m.HandleKeyMsg(msg)
@@ -19,15 +22,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.stdoutOutput.SetContent(strings.Join(m.messages, "")) m.stdoutOutput.SetContent(strings.Join(m.messages, ""))
m.stdoutOutput.GotoBottom() m.stdoutOutput.GotoBottom()
return m, ListenBridge(m.listenBridge) return m, ListenBridge(m.listenBridge)
case ExecutionStoppedMsg: case codeviewer.ExecutionStoppedMsg:
m.currExecutionPoint = bridge.ExecutionPoint(msg)
return m, tea.Batch(ListenBridgeExecutionsStopped(m.listenBridgeExecutionsStopped), m.GetLocals()) return m, tea.Batch(ListenBridgeExecutionsStopped(m.listenBridgeExecutionsStopped), m.GetLocals())
case LocalsMsg: case LocalsMsg:
m.currLocals = map[string]any(msg) m.currLocals = map[string]any(msg)
m.localsViewer.SetContent(strings.Join(flattenDict(m.currLocals, 0), "\n")) m.localsViewer.SetNewVariables(m.currLocals)
} }
return m, nil return m, cmd
} }
func (m Model) GetLocals() tea.Cmd { func (m Model) GetLocals() tea.Cmd {
@@ -50,7 +52,7 @@ func (m Model) UpdateWindowSize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) {
outputHeight := msg.Height - editorHeight - 4 outputHeight := msg.Height - editorHeight - 4
m.codeViewer.Width = msg.Width m.codeViewer.Width = msg.Width
m.codeViewer.Height = editorHeight m.codeViewer.Height = editorHeight - 2
m.stdoutOutput.Width = msg.Width / 2 m.stdoutOutput.Width = msg.Width / 2
m.stdoutOutput.Height = outputHeight m.stdoutOutput.Height = outputHeight
@@ -67,16 +69,13 @@ func (m Model) HandleKeyMsg(key tea.KeyMsg) (tea.Model, tea.Cmd) {
switch key.String() { switch key.String() {
case "q", "ctrl+c": case "q", "ctrl+c":
return m, tea.Quit return m, tea.Quit
case "k":
m.codeViewer.ScrollUp(1)
m.cursor = max(0, m.cursor-1)
case "j":
m.codeViewer.ScrollDown(1)
m.cursor = min(m.textLines-1, m.cursor+1)
case "b": case "b":
lineNumber := m.cursor + 1 lineNumber := m.codeViewer.Cursor + 1
m.bridge.Breakpoint(m.currentFile, lineNumber) set, err := m.bridge.Breakpoint(m.currentFile, lineNumber)
if err != nil {
slog.Error("could not set or unset breakpoint", "error", err)
}
if file, ok := m.breakpoints[m.currentFile]; ok { if file, ok := m.breakpoints[m.currentFile]; ok {
m.breakpoints[m.currentFile] = append(file, lineNumber) m.breakpoints[m.currentFile] = append(file, lineNumber)
} else { } else {
@@ -84,6 +83,13 @@ func (m Model) HandleKeyMsg(key tea.KeyMsg) (tea.Model, tea.Cmd) {
lineNumber, lineNumber,
} }
} }
return m, func() tea.Msg {
// check if this is in currently viewed file
return codeviewer.BreakpointMsg{
Added: set,
Line: lineNumber,
}
}
case "s": case "s":
m.bridge.Step() m.bridge.Step()
case "c": case "c":

View File

@@ -0,0 +1,53 @@
package variablesviewer
import (
"fmt"
"slices"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
type VariableViewer struct {
variables map[string]any
Width, Height int
}
func NewVariableViewer(variables map[string]any) VariableViewer {
return VariableViewer{
variables: variables,
Width: 0,
Height: 0,
}
}
func (v *VariableViewer) SetNewVariables(variables map[string]any) {
v.variables = variables
}
func (v VariableViewer) Init() tea.Cmd {
return nil
}
func (v VariableViewer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return v, nil
}
func (v VariableViewer) View() string {
out := strings.Builder{}
keys := make([]string, 0, len(v.variables))
for k := range v.variables {
keys = append(keys, k)
}
slices.Sort(keys)
// TODO make this listen to Height correctly
for _, key := range keys {
value := v.variables[key]
fmt.Fprintf(&out, "> %s: %s\n", key, value)
}
return out.String()
}

View File

@@ -1,14 +1,9 @@
package ui package ui
import ( import (
"bytes"
"fmt" "fmt"
"slices"
"strings" "strings"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
@@ -19,7 +14,7 @@ func flattenDict(m map[string]interface{}, indent int) []string {
prefix := strings.Repeat(" ", indent) prefix := strings.Repeat(" ", indent)
for k, v := range m { for k, v := range m {
switch val := v.(type) { switch val := v.(type) {
case map[string]interface{}: case map[string]any:
lines = append(lines, fmt.Sprintf("%s%s:", prefix, k)) lines = append(lines, fmt.Sprintf("%s%s:", prefix, k))
lines = append(lines, flattenDict(val, indent+1)...) lines = append(lines, flattenDict(val, indent+1)...)
default: default:
@@ -30,39 +25,6 @@ func flattenDict(m map[string]interface{}, indent int) []string {
} }
func (m Model) View() string { func (m Model) View() string {
var buf bytes.Buffer
lexer := lexers.Get("python")
style := styles.Get("monokai")
formatter := formatters.Get("terminal16m")
iterator, _ := lexer.Tokenise(nil, m.text)
formatter.Format(&buf, style, iterator)
var out strings.Builder
lines := strings.Split(buf.String(), "\n")
breakpoints := m.breakpoints[m.currentFile]
for i, line := range lines {
breakpoint := " "
cursor := " "
executor := " "
if slices.Contains(breakpoints, i+1) {
breakpoint = "O"
}
if i+1 == m.currExecutionPoint.Line {
executor = "!"
}
if i == m.cursor {
cursor = ">"
}
fmt.Fprintf(&out, "%-4d%s%s%s %s\n", i+1, breakpoint, executor, cursor, line)
}
m.codeViewer.SetContent(out.String())
hFrame, wFrame := panelStyle.GetFrameSize() hFrame, wFrame := panelStyle.GetFrameSize()
topPanel := panelStyle. topPanel := panelStyle.
Height(m.codeViewer.Height - hFrame). Height(m.codeViewer.Height - hFrame).