racket/collects/gui-debugger/debug-tool.ss
Ryan Culpepper ff9ebbde05 fix PR 9398: debugger and macro stepper buttons get out of sync wrt tabs
move enable/disable-evaluation extensions from tab mixin to unit-frame mixin

svn: r14118
2009-03-16 04:40:30 +00:00

1418 lines
72 KiB
Scheme

(module debug-tool mzscheme
(require mzlib/etc
mzlib/list
mzlib/class
mzlib/unit
mzlib/contract
mred
mzlib/match
drscheme/tool
"marks.ss"
mrlib/switchable-button
mrlib/bitmap-label
"annotator.ss"
"load-sandbox.ss"
framework
string-constants
lang/debugger-language-interface)
(provide tool@)
; QUESTIONS/IDEAS
; what is the right way to deal with macros?
; how can the three tool classes communicate with each other safely
(define tool@
(unit
(import drscheme:tool^)
(export drscheme:tool-exports^)
(define (phase1)
(drscheme:language:extend-language-interface
debugger-language<%>
(lambda (superclass)
(class* superclass (debugger-language<%>)
(public debugger:supported?)
(define (debugger:supported?) #t)
(super-instantiate ())))))
(define phase2 void)
(define (extract-language-level settings)
(drscheme:language-configuration:language-settings-language settings))
(define (debugger-does-not-work-for? lang)
(not (send lang debugger:supported?)))
(define (robust-syntax-source stx)
(and (syntax? stx) (syntax-source stx)))
(define (robust-vector-ref vec idx)
(if (< idx (vector-length vec))
(vector-ref vec idx)
#f))
(define (safe-vector-set! vec idx val)
(when (< idx (vector-length vec))
(vector-set! vec idx val))
(void))
(define (truncate str n)
(cond [(< (string-length str) n) str]
[(>= n 3) (string-append
(substring str 0 (- n 3))
"...")]
[else (substring str 0 (min n (string-length str)))]))
(define (clean-status s)
(truncate (regexp-replace* #rx"\n" s " ") 200))
(define (index-of chr str)
(let loop ([i 0])
(if (< i (string-length str))
(if (char=? chr (string-ref str i))
i
(loop (add1 i)))
#f)))
(define (safe-min . args)
(apply min (filter identity args)))
;; trim-expr-str: string -> string
;; examples:
;; short-id => short-id
;; really-long-identifier => really-lon...
;; (<form>) => (<form>)
;; (<form> <arg1> ... <argn>) => (<form> ...)
(define trim-expr-str
(opt-lambda (str [len 10])
(let* ([strlen (string-length str)]
[starts-with-paren (and (> strlen 0)
(char=? (string-ref str 0) #\())]
[len2 (+ len 4)]
[trunc-pos (safe-min (index-of #\space str)
(index-of #\newline str)
(and (> strlen len2) len)
strlen)])
(if (>= trunc-pos strlen)
str
(string-append
(substring str 0 trunc-pos)
(if starts-with-paren
" ...)"
" ..."))))))
(define (average . values)
(/ (apply + values) (length values)))
(define (truncate-value v size depth)
(cond
[(zero? depth) '...]
[(and (string? v)
(> (string-length v) size))
(string-append (substring v 0 size) "...")]
[(list? v)
(let* ([len (length v)]
[res (build-list (min size len)
(lambda (i) (truncate-value (list-ref v i) size (sub1 depth))))])
(if (> len size) (append res (list '...)) res))]
[(vector? v)
(build-vector (min size (vector-length v))
(lambda (i)
(if (and (= i (sub1 size))
(> size (vector-length v)))
'...
(truncate-value (vector-ref v i) size (sub1 depth)))))]
[else v]))
(define filename->defs
(opt-lambda (source [default #f])
(let/cc k
(cond
[(is-a? source editor<%>) source]
[else
(send (group:get-the-frame-group) for-each-frame
(lambda (frame)
(and (is-a? frame drscheme:unit:frame<%>)
(let* ([defss (map (lambda (t) (send t get-defs)) (send frame get-tabs))]
[defs (findf (lambda (d) (send d port-name-matches? source)) defss)])
(and defs (k defs))))))
default]))))
(define (debug-definitions-text-mixin super%)
(class super%
(inherit dc-location-to-editor-location
editor-location-to-dc-location
invalidate-bitmap-cache
begin-edit-sequence
end-edit-sequence
get-canvas
get-top-level-window
get-filename
get-tab)
(define mouse-over-pos #f)
;; pen and brush for drawing a breakpoint
(define bp-pen (send the-pen-list find-or-create-pen "black" 1 'solid))
(define bp-brush (send the-brush-list find-or-create-brush "red" 'solid))
;; pen and brush for marking a location that could have a breakpoint installed
(define bp-mo-pen (send the-pen-list find-or-create-pen "darkgray" 1 'solid))
(define bp-mo-brush (send the-brush-list find-or-create-brush "tomato"
'solid))
;; pen and brush for marking a conditional breakpoint
(define bp-tmp-pen (send the-pen-list find-or-create-pen "black" 1 'solid))
(define bp-tmp-brush (send the-brush-list find-or-create-brush "yellow"
'solid))
;; pen and brush for drawing the normal execution location
(define pc-pen (send the-pen-list find-or-create-pen "black" 1 'solid))
(define pc-brush (send the-brush-list find-or-create-brush "forestgreen" 'solid))
;; pen and brush for marking the expression when not at the top of the stack
(define pc-up-stack-pen (send the-pen-list find-or-create-pen "black" 1 'solid))
(define pc-up-stack-brush (send the-brush-list find-or-create-brush "lightgreen" 'solid))
;; pen and brush for marking the location when there's an an error
(define pc-err-pen (send the-pen-list find-or-create-pen "black" 1 'solid))
(define pc-err-brush (send the-brush-list find-or-create-brush "red" 'solid))
;; pen and brush for marking the location following a break
(define pc-brk-pen (send the-pen-list find-or-create-pen "black" 1 'solid))
(define pc-brk-brush (send the-brush-list find-or-create-brush "gray" 'solid))
(super-instantiate ())
(define/augment (on-delete start len)
(begin-edit-sequence)
(let ([breakpoints (send (get-tab) get-breakpoints)]
[shifts empty])
(hash-table-for-each
breakpoints
(lambda (pos status)
(cond
; deletion after breakpoint: no effect
[(<= pos start)]
; deletion of breakpoint: remove from table
[(and (< start pos)
(<= pos (+ start len)))
(hash-table-remove! breakpoints pos)]
; deletion before breakpoint: shift breakpoint
[(> pos (+ start len))
(hash-table-remove! breakpoints pos)
(set! shifts (cons (cons (- pos len) status) shifts))])))
(for-each (lambda (p) (hash-table-put! breakpoints (car p) (cdr p)))
shifts))
(inner (void) on-delete start len))
(define/augment (after-delete start len)
(inner (void) after-delete start len)
(when (send (get-tab) debug?)
(send (get-tab) hide-debug))
(end-edit-sequence))
(define/augment (on-insert start len)
(begin-edit-sequence)
(let ([breakpoints (send (get-tab) get-breakpoints)]
[shifts empty])
(hash-table-for-each
breakpoints
(lambda (pos status)
(when (< start pos)
;; text inserted before this breakpoint, so shift
;; the breakpoint forward by <len> positions
(hash-table-remove! breakpoints pos)
(set! shifts (cons (cons (+ pos len) status) shifts)))))
;; update the breakpoint locations
(for-each (lambda (p) (hash-table-put! breakpoints (car p) (cdr p)))
shifts))
(inner (void) on-insert start len))
(define/augment (after-insert start len)
(inner (void) after-insert start len)
(when (send (get-tab) debug?)
(send (get-tab) hide-debug))
(end-edit-sequence))
;; lookup id in the given set of stack frames;
;; if that fails, try the top-level environment
;; invokes sk on success, fk on failure
(define (lookup-var id frames sk fk)
(cond
[(and id frames (lookup-first-binding
(lambda (id2) (module-identifier=? id id2))
frames (lambda () #f)))
=> (lambda (binding)
(sk (mark-binding-value binding)
(lambda (v) (mark-binding-set! binding v))))]
[(and id (send (get-tab) lookup-top-level-var
id (lambda () #f)))
=> (lambda (tlb)
(sk (tlb) tlb))]
[else (fk)]))
;; mouse-event -> (or (values #f #f) (values pos editor))
(define/private (get-pos/text event)
(let ([event-x (send event get-x)]
[event-y (send event get-y)]
[on-it? (box #f)])
(let loop ([editor this])
(let-values ([(x y) (send editor dc-location-to-editor-location
event-x event-y)])
(cond
[(is-a? editor text%)
(let ([pos (send editor find-position x y #f on-it?)])
(cond
[(not (unbox on-it?)) (values #f #f)]
[else
(let ([snip (send editor find-snip pos 'after-or-none)])
(if (and snip
(is-a? snip editor-snip%))
(loop (send snip get-editor))
(values pos editor)))]))]
[(is-a? editor pasteboard%)
(let ([snip (send editor find-snip x y)])
(if (and snip
(is-a? snip editor-snip%))
(loop (send snip get-editor))
(values #f #f)))]
[else (values #f #f)])))))
;; text% start end -> (values left top right bottom)
;; (four numbers that indicate the locations in pixels of the
;; box bounding the text between start and end
(define/private (find-char-box text left-pos right-pos)
(let ([xlb (box 0)]
[ylb (box 0)]
[xrb (box 0)]
[yrb (box 0)])
(send text position-location left-pos xlb ylb #t)
(send text position-location right-pos xrb yrb #f)
(let*-values ([(xl-off yl-off) (send text editor-location-to-dc-location
(unbox xlb) (unbox ylb))]
[(xl yl) (dc-location-to-editor-location xl-off yl-off)]
[(xr-off yr-off) (send text editor-location-to-dc-location
(unbox xrb) (unbox yrb))]
[(xr yr) (dc-location-to-editor-location xr-off yr-off)])
(values xl yl xr yr))))
(define/private (render v)
(send (get-tab) render v))
;; mouse-event% integer -> ()
;; handles a right-click on a position that's not a breakable paren
(define (debugger-handle-right-click-non-breakable event pos)
(let* ([frames (send (get-tab) get-stack-frames)]
[pos-vec (send (get-tab) get-pos-vec)]
[id (robust-vector-ref pos-vec pos)])
(unless (lookup-var
id frames
(lambda (val wr)
(let ([id-sym (syntax-e id)]
[menu (make-object popup-menu% #f)])
(make-object menu-item%
(clean-status (format "Print value of ~a to console" id-sym))
menu
(lambda (item evt)
(send (get-tab) print-to-console (format "~a = ~s" id-sym val))))
(make-object menu-item% (format "(set! ~a ...)" id-sym) menu
(lambda (item evt)
(let* ([tmp (get-text-from-user
(format "New value for ~a" id-sym) #f #f
(format "~a" val))])
(when tmp
(let/ec k
(wr (with-handlers
([exn:fail?
(lambda (exn)
(message-box
"Debugger Error"
(format "The following error occurred: ~a"
(exn-message exn)))
(k))])
(read (open-input-string tmp)))))))))
(send (get-canvas) popup-menu menu
(+ 1 (inexact->exact (floor (send event get-x))))
(+ 1 (inexact->exact (floor (send event get-y)))))
#t))
(lambda () #f))
(super on-event event))))
(define (debugger-handle-right-click-breakable event breakpoints pos break-status)
(let ([menu (make-object popup-menu% #f)])
(make-object menu-item%
(if break-status
"Remove pause at this point"
"Pause at this point")
menu
(lambda (item evt)
(hash-table-put! breakpoints pos (not break-status))
(invalidate-bitmap-cache)))
(let ([pc (send (get-tab) get-pc)])
(if (and pc (= pos pc))
(let* ([stat (send (get-tab) get-break-status)]
[f (get-top-level-window)]
[rendered-value
(if (cons? stat)
(if (= 2 (length stat))
(render (cadr stat))
(format "~s" (cons 'values
(map (lambda (v) (render v)) (rest stat)))))
"")])
(when (cons? stat)
(make-object menu-item%
"Print return value to console" menu
(lambda _ (send (get-tab) print-to-console
(string-append "return val = " rendered-value)))))
(when (not (eq? stat 'break))
(make-object menu-item%
(if (cons? stat)
"Change return value..."
"Skip expression...")
menu
(lambda (item evt)
(let ([tmp (get-text-from-user "Return value" #f)])
(when tmp
(let/ec k
(send (get-tab) set-break-status
(cons 'exit-break
(call-with-values
(lambda ()
(with-handlers
([exn:fail?
(lambda (exn)
(message-box
"Debugger Error"
(format "An error occurred: ~a" (exn-message exn))
#f '(ok))
(k))])
(read (open-input-string tmp))))
list)))
(invalidate-bitmap-cache))))))))
(make-object menu-item%
"Continue to this point"
menu
(lambda (item evt)
(hash-table-put!
breakpoints pos
(lambda () (hash-table-put! breakpoints pos break-status) #t))
(invalidate-bitmap-cache)
(when (send (get-tab) get-stack-frames)
(send (get-tab) resume))))))
(send (get-canvas) popup-menu menu
(+ 1 (inexact->exact (floor (send event get-x))))
(+ 1 (inexact->exact (floor (send event get-y)))))))
(define (debugger-handle-right-click event breakpoints)
(let-values ([(pos text) (get-pos/text event)])
(if (and pos text)
(let* ([pos (add1 pos)]
[break-status (hash-table-get breakpoints pos (lambda () 'invalid))])
(match break-status
[(or #t #f (? procedure?))
(debugger-handle-right-click-breakable event breakpoints pos break-status)]
['invalid
(debugger-handle-right-click-non-breakable event pos)]))
(super on-event event))))
(define (debugger-handle-event event)
(let ([breakpoints (send (get-tab) get-breakpoints)])
(cond
[(send event leaving?)
(when mouse-over-pos
(set! mouse-over-pos #f)
(invalidate-bitmap-cache))
(super on-event event)]
[(or (send event moving?)
(send event entering?))
(let-values ([(pos text) (get-pos/text event)])
(when (and pos text)
(let ([pos (add1 pos)])
(cond
;; mouse on breakable pos and hasn't moved significantly
[(eq? pos mouse-over-pos)]
;; mouse on new breakable pos
[(not (eq? (hash-table-get
breakpoints pos (lambda () 'invalid)) 'invalid))
(set! mouse-over-pos pos)
(invalidate-bitmap-cache)]
;; moved off breakable pos
[mouse-over-pos
(set! mouse-over-pos #f)
(invalidate-bitmap-cache)])
(let* ([frames (send (get-tab) get-stack-frames)]
[pos-vec (send (get-tab) get-pos-vec)]
[id (robust-vector-ref pos-vec pos)])
(send (get-tab) set-mouse-over-msg
(clean-status
(lookup-var id (list-tail frames (send (get-tab) get-frame-num))
;; id found
(lambda (val _)
(string-append (symbol->string (syntax-e id)) " = " (render val)))
;; id not found
(lambda () ""))))))))
(super on-event event)]
[(send event button-down? 'right)
(debugger-handle-right-click event breakpoints)]
[else (super on-event event)])))
(define/override (on-event event)
(if (send (get-tab) debug?)
(debugger-handle-event event)
(super on-event event)))
(define/override (on-paint before dc left top right bottom dx dy draw-caret)
(super on-paint before dc left top right bottom dx dy draw-caret)
(when (and (send (get-tab) debug?) (not before))
;; render breakpoints
(let ([breakpoints (send (get-tab) get-breakpoints)])
(hash-table-for-each
breakpoints
(lambda (pos enabled?)
(when (and (>= pos 0) (or enabled? (and mouse-over-pos (= mouse-over-pos pos))))
(let*-values ([(xl yl xr yr) (find-char-box this (sub1 pos) pos)]
[(diameter) (- xr xl)]
[(yoff) (/ (- yr yl diameter) 2)])
(let ([op (send dc get-pen)]
[ob (send dc get-brush)])
(case enabled?
[(#t) (send dc set-pen bp-pen)
(send dc set-brush bp-brush)]
[(#f) (send dc set-pen bp-mo-pen)
(send dc set-brush bp-mo-brush)]
[else (send dc set-pen bp-tmp-pen)
(send dc set-brush bp-tmp-brush)])
(send dc draw-ellipse (+ xl dx) (+ yl dy yoff) diameter diameter)
(send dc set-pen op)
(send dc set-brush ob)))))))
;; mark the boundaries of the current stack frame
;; unless we're at the end of the expression and looking at the top frame,
;; in which case just mark the current location
(let* ([frame-defs (send (get-tab) defs-containing-current-frame)]
[pos (send (get-tab) get-current-frame-endpoints)]
[start (and pos (first pos))]
[end (and pos (second pos))]
[frame-num (send (get-tab) get-frame-num)]
[break-status (send (get-tab) get-break-status)])
(when (and (eq? frame-defs this) start end)
(let*-values ([(xl yl xr yr) (find-char-box this (sub1 start) start)]
[(ym) (average yl yr)]
[(xa ya xb yb) (find-char-box this (sub1 end) end)]
[(diameter) (- xb xa)]
[(yoff) (/ (- yb ya diameter) 2)]
[(ym2) (average ya yb)])
(let ([op (send dc get-pen)]
[ob (send dc get-brush)])
(cond
[(and (zero? frame-num)
(eq? break-status 'error))
(send dc set-pen pc-err-pen)
(send dc set-brush pc-err-brush)]
[(and (zero? frame-num)
(eq? break-status 'break))
(send dc set-pen pc-brk-pen)
(send dc set-brush pc-brk-brush)]
[(zero? frame-num)
(send dc set-pen pc-pen)
(send dc set-brush pc-brush)]
[else
(send dc set-pen pc-up-stack-pen)
(send dc set-brush pc-up-stack-brush)])
(unless (and (zero? frame-num) (cons? break-status))
;; mark the beginning of the expression with a triangle
(send dc draw-polygon (list (make-object point% xl yl)
(make-object point% xl yr)
(make-object point% xr ym)) dx dy))
(if (and (zero? frame-num) (cons? break-status))
;; top frame, end: mark the end of the expression with a triangle
(send dc draw-polygon (list (make-object point% xa ya)
(make-object point% xa yb)
(make-object point% xb ym2)) dx dy)
;; otherwise: make the end of the expression with a circle
(send dc draw-ellipse (+ xa dx) (+ ya dy yoff) diameter diameter))
(send dc set-pen op)
(send dc set-brush ob)))))))
(define/augment (after-set-next-settings s)
(let ([tlw (get-top-level-window)])
(when tlw
(send tlw check-current-language-for-debugger)))
(inner (void) after-set-next-settings s))))
(define (debug-interactions-text-mixin super%)
(class super%
(inherit run-in-evaluation-thread)
(super-instantiate ())
(define debugged-thread #f)
(define tab #f)
(define/public (get-tab) tab)
(define/public (set-tab t) (set! tab t))
(define/private (stx-source->breakpoints src)
(send (send (or (and src (filename->defs src)) this) get-tab) get-breakpoints))
(define/private (stx-source->pos-vec src)
(send (send (or (and src (filename->defs src)) this) get-tab) get-pos-vec))
;; make-debug-eval-handler : (sexp -> value) -> sexp -> value
;; adds debugging information to `sexp' and calls `oe'
(define/private (make-debug-eval-handler oe break? break-before break-after)
(lambda (orig-exp)
(if (compiled-expression? (if (syntax? orig-exp)
(syntax-e orig-exp)
orig-exp))
(oe orig-exp)
(let* ([exp (if (syntax? orig-exp)
orig-exp
(namespace-syntax-introduce
(datum->syntax-object #f orig-exp)))]
[top-e (expand-syntax-to-top-form exp)])
(parameterize ([current-eval oe])
(eval/annotations
top-e
; annotate-module?
(lambda (fn m)
(cond
[(filename->defs fn)
=>
; fn is loaded into defs
(lambda (defs)
(let ([extern-tab (send defs get-tab)]
[this-tab (get-tab)])
(case (if (or (not (send extern-tab debug?))
(eq? this-tab (send extern-tab get-master)))
(message-box
"Debugging Multi-File Program"
(format "Debug ~a?" fn)
#f
'(yes-no))
(message-box
"Debugging Multi-File Program"
(format "~a is already involved in a debugging session." fn)
#f
'(ok)))
[(yes)
; set tab up with shared data from the master tab
(send extern-tab prepare-execution #t)
(send this-tab add-slave extern-tab)
(call-with-values
(lambda () (send this-tab get-shared-data))
(lambda vals (send extern-tab set-shared-data . vals)))
#t]
[(no ok)
(send extern-tab prepare-execution #f)
#f])))]
; fn is not open, so don't try to debug it
[else #f]))
; annotator
(lambda (stx)
(let*-values ([(source) (syntax-source stx)]
[(breakpoints) (stx-source->breakpoints source)]
[(pos-vec) (stx-source->pos-vec source)]
[(annotated break-posns)
(annotate-for-single-stepping
(expand-syntax stx)
break? break-before break-after
; record-bound-identifier
(lambda (type bound binding)
(cond
[(filename->defs (robust-syntax-source bound))
=>
(lambda (defs)
(let ([pos-vec (send (send defs get-tab) get-pos-vec)])
(let loop ([i 0])
(when (< i (syntax-span bound))
(safe-vector-set! pos-vec (+ i (syntax-position bound))
binding)
(loop (add1 i))))))]
[else (void)]))
; record-top-level-identifier
(lambda (mod var rd/wr)
; filename->defs should succeed unless a slave tab gets closed
(cond
[(filename->defs (robust-syntax-source var))
=>
(lambda (defs)
(send (send defs get-tab)
add-top-level-binding var rd/wr))]
[else (void)]))
source)])
(hash-table-for-each
breakpoints
(lambda (pos status)
; possible efficiency problem for large files with many breakpoints
(when (and (syntax-position stx)
(>= pos (syntax-position stx))
(< pos (+ (syntax-position stx) (syntax-span stx)))
(not (memq pos break-posns)))
(hash-table-remove! breakpoints pos))))
(for-each (lambda (posn)
(hash-table-put!
breakpoints posn
(hash-table-get breakpoints posn (lambda () #f)))) break-posns)
annotated))))))))
(define/override (reset-console)
(super reset-console)
(let ([tab (get-tab)])
(when (and tab (send tab debug?))
(let ([breakpoints (send tab get-breakpoints)])
(run-in-evaluation-thread
(lambda ()
;(print-struct #t)
(let ([self (current-thread)]
[oeh (uncaught-exception-handler)]
[err-hndlr (error-display-handler)])
(set! debugged-thread self)
(error-display-handler
(lambda (msg exn)
(err-hndlr msg exn)
(if (and (eq? self (current-thread)) (exn:fail? exn))
(send (get-tab) suspend oeh
(continuation-mark-set->list (exn-continuation-marks exn) debug-key)
'error)))) ; this breaks the buttons because it looks like we can resume
(current-eval
(make-debug-eval-handler
(current-eval)
; break? -- curried to avoid looking up defs from source each time
(lambda (src)
(let* ([defs (filename->defs src)]
[src-tab (if defs (send defs get-tab) (get-tab))]
[breakpoints (if src
(send src-tab get-breakpoints)
breakpoints)]
[single-step? (send tab get-single-step-box)]
[closed? (send src-tab get-closed-box)])
(lambda (pos)
(and (not (unbox closed?))
(or (unbox single-step?)
(let ([bp (hash-table-get breakpoints pos #f)])
(if (procedure? bp)
(bp)
bp)))))))
; break-before
(lambda (top-mark ccm)
(let* ([debug-marks (continuation-mark-set->list ccm debug-key)])
(send (get-tab) suspend oeh (cons top-mark debug-marks) 'entry-break)))
; break-after
(case-lambda
[(top-mark ccm val)
(let* ([debug-marks (continuation-mark-set->list ccm debug-key)])
(car (send (get-tab) suspend oeh (cons top-mark debug-marks)
(list 'exit-break val))))]
[(top-mark ccm . vals)
(let* ([debug-marks (continuation-mark-set->list ccm debug-key)])
(apply values
(send (get-tab) suspend oeh (cons top-mark debug-marks)
(cons 'exit-break vals))))])))
(uncaught-exception-handler
(lambda (exn)
(if (and (exn:break? exn) (send (get-tab) suspend-on-break?))
(let ([marks (exn-continuation-marks exn)]
[cont (exn:break-continuation exn)])
(send (get-tab) suspend oeh (continuation-mark-set->list marks debug-key) 'break)
(cont))
(oeh exn)))))))))))))
(define (debug-tab-mixin super%)
(class super%
(inherit get-defs
get-ints
get-frame)
(field [breakpoints (make-hash-table)]
[suspend-sema (make-semaphore 1)]
[resume-ch (make-channel)]
[in-user-ch (make-channel)]
[want-suspend-on-break? #f]
[want-debug? #f]
[master this]
[slaves empty]
[closed? (box #f)]
[stack-frames (box #f)]
[frame-num (box 0)]
[break-status (box #f)]
[current-language-settings #f]
[pos-vec (vector #f)]
[single-step? (box #t)]
[top-level-bindings empty]
[control-panel #f])
(define/public (debug?) want-debug?)
(define/public (get-master) master)
(define/public (add-slave s)
(set! slaves (cons s slaves)))
(define/public (get-closed-box) closed?)
(define/public suspend-on-break?
(case-lambda
[() want-suspend-on-break?]
[(v) (set! want-suspend-on-break? v)]))
(define/public (get-stack-frames)
(unbox stack-frames))
(define/public (get-pos-vec) pos-vec)
(define/public (get-breakpoints) breakpoints)
(define/public (get-break-status) (unbox break-status))
(define/public (get-frame-num) (unbox frame-num))
(define/public (set-shared-data bs sf sema res-ch usr-ch step? frame m)
(set! break-status bs)
(set! stack-frames sf)
(set! suspend-sema sema)
(set! resume-ch res-ch)
(set! in-user-ch usr-ch)
(set! single-step? step?)
(set! frame-num frame)
(set! master m))
(define/public (get-shared-data)
(values break-status stack-frames suspend-sema resume-ch in-user-ch single-step? frame-num master))
(define/public (get-single-step-box) single-step?)
(define/public (set-single-step?! v) (set-box! single-step? v))
(define/public (set-break-status stat) (set-box! break-status stat))
(define/public (add-top-level-binding var rd/wr)
(set! top-level-bindings (cons (cons var rd/wr) top-level-bindings)))
(define/public (lookup-top-level-var var failure-thunk)
(let loop ([bindings top-level-bindings])
(cond
[(empty? bindings) (failure-thunk)]
[(or (bound-identifier=? var (caar bindings))
(free-identifier=? var (caar bindings))) (cdar bindings)]
[else (loop (rest bindings))])))
(define/public (move-to-frame the-frame-num)
(set-box! frame-num the-frame-num)
(suspend-gui (get-stack-frames) (get-break-status) #t #t))
(define/public (resume)
(let ([v (get-break-status)])
(resume-gui)
(channel-put resume-ch (and (pair? v) (cdr v)))))
(define/public (set-mouse-over-msg msg)
(send (get-frame) set-mouse-over-msg msg))
(define/public (defs-containing-pc)
(let ([stack-frames (get-stack-frames)])
(and (cons? stack-frames)
(let* ([src-stx (mark-source (first stack-frames))]
[source (syntax-source src-stx)])
(if source
(filename->defs source)
(get-defs))))))
(define/public (defs-containing-current-frame)
(let ([stack-frames (get-stack-frames)])
(and (cons? stack-frames)
(let* ([src-stx (mark-source (list-ref stack-frames (get-frame-num)))]
[source (syntax-source src-stx)])
(if source
(filename->defs source)
(get-defs))))))
(define/public (get-pc)
(let ([stack-frames (get-stack-frames)])
(and (cons? stack-frames)
(let* ([src-stx (mark-source (first stack-frames))]
[start (syntax-position src-stx)]
[end (and start (+ start (syntax-span src-stx) -1))])
(if (cons? (get-break-status))
end
start)))))
(define/public (get-frame-endpoints frame-num)
(let ([stack-frames (get-stack-frames)])
(and (cons? stack-frames)
(let* ([src-stx (mark-source (list-ref stack-frames frame-num))]
[start (syntax-position src-stx)]
[end (and start (+ start (syntax-span src-stx) -1))])
(list start end)))))
(define/public (get-current-frame-endpoints)
(get-frame-endpoints (get-frame-num)))
(define (do-in-user-thread thunk)
(if (get-break-status)
(channel-put in-user-ch thunk)
(send (get-ints) run-in-evaluation-thread thunk)))
(define/public (render v)
;; ==drscheme eventspace thread==
;; only when a user thread is suspended
(let ([result-ch (make-channel)]
[v (truncate-value v 100 5)])
(do-in-user-thread
(lambda ()
(let ([s (open-output-string)])
(send (drscheme:language-configuration:language-settings-language
current-language-settings)
render-value
v
(drscheme:language-configuration:language-settings-settings
current-language-settings)
s)
(channel-put result-ch (get-output-string s)))))
(channel-get result-ch)))
(define/public (print-to-console v)
;; ==drscheme eventspace thread==
;; only when a user thread is suspended
(do-in-user-thread (lambda () (fprintf (current-error-port) " ### DEBUGGER: ~s~n" v))))
(define (frame->end-breakpoint-status frame)
(let/ec k
(let* ([stx (mark-source frame)]
[src (syntax-source stx)]
[pos (if (not (syntax-position stx))
(k 'invalid)
(+ (syntax-position stx) (syntax-span stx) -1))]
[defs (filename->defs src)]
[tab (if defs (send defs get-tab) (k 'invalid))]
[bps (send tab get-breakpoints)])
(hash-table-get bps pos 'invalid))))
(define (can-step-over? frames status)
(and (or (not (zero? (get-frame-num))) (eq? status 'entry-break))
frames
(not (empty? frames))
(not (eq? (frame->end-breakpoint-status (list-ref frames (get-frame-num))) 'invalid))))
(define (can-step-out? frames status)
(and frames
(let ([frames (list-tail frames (get-frame-num))])
(and (not (empty? frames))
(ormap (lambda (f) (not (eq? (frame->end-breakpoint-status f) 'invalid)))
(rest frames))))))
(define/public suspend-gui
(opt-lambda (frames status [switch-tabs? #f] [already-stopped? #f])
(let ([top-of-stack? (zero? (get-frame-num))]
[status-message (send (get-frame) get-status-message)])
(set! want-suspend-on-break? #f)
(set-single-step?! #f)
(set-box! stack-frames frames)
(set-box! break-status status)
(send (send (get-frame) get-pause-button) enable #f)
(send (send (get-frame) get-step-button) enable top-of-stack?)
(send (send (get-frame) get-step-over-button) enable (can-step-over? frames status))
(send (send (get-frame) get-step-out-button) enable (can-step-out? frames status))
(send (send (get-frame) get-resume-button) enable #t)
(when (cons? frames)
(send (get-frame) register-stack-frames frames already-stopped?)
(send (get-frame) register-vars (list-ref frames (get-frame-num))))
(send status-message set-label
(if (and (cons? status) top-of-stack?)
(let ([expr (mark-source (first frames))])
(cond
; should succeed unless the user closes a slave tab during debugging
[(filename->defs (syntax-source expr))
=> (lambda (defs)
(clean-status
(string-append
(if (syntax-position expr)
(trim-expr-str
(send defs get-text
(sub1 (syntax-position expr))
(+ -1 (syntax-position expr) (syntax-span expr))))
"??")
" => "
(if (= 2 (length status))
(render (cadr status))
(string-append
"(values"
(let loop ([vals (rest status)])
(cond
[(cons? vals) (string-append " " (render (first vals))
(loop (rest vals)))]
[else ")"])))))))]
[""]))
""))
(cond [(get-current-frame-endpoints)
=> (lambda (start/end)
(cond [(and (first start/end) (defs-containing-current-frame))
=> (lambda (defs)
(cond
[(and switch-tabs? (send defs get-filename))
=> (lambda (fn) (handler:edit-file fn))])
(send defs scroll-to-position (first start/end)))]))])
(send (get-defs) invalidate-bitmap-cache))))
(define/public (resume-gui)
(set-box! stack-frames #f)
(set-box! break-status #f)
(set-box! frame-num 0)
(send (send (get-frame) get-pause-button) enable #t)
(send (send (get-frame) get-step-button) enable #f)
(send (send (get-frame) get-step-over-button) enable #f)
(send (send (get-frame) get-step-out-button) enable #f)
(send (send (get-frame) get-resume-button) enable #f)
(send (send (get-frame) get-status-message) set-label "")
(send (get-frame) clear-stack-frames/vars)
(send (get-defs) invalidate-bitmap-cache))
(define/public suspend
;; ==called from user thread==
(opt-lambda (break-handler frames [status #f])
;; suspend-sema ensures that we allow only one suspended thread
;; at a time
(cond
[(semaphore-try-wait? suspend-sema)
(parameterize ([current-eventspace (send (get-frame) get-eventspace)])
(queue-callback (lambda () (suspend-gui frames status #t))))
(with-handlers ([exn:break?
(lambda (exn)
(let ([wait-sema (make-semaphore)])
(parameterize ([current-eventspace (send (get-frame) get-eventspace)])
(queue-callback (lambda ()
(resume-gui)
(semaphore-post wait-sema))))
(semaphore-wait wait-sema))
(semaphore-post suspend-sema)
(break-handler exn))])
(begin0
(let loop ()
(sync/enable-break
resume-ch (handle-evt in-user-ch (lambda (thunk)
(thunk)
(loop)))))
(semaphore-post suspend-sema)))]
[(pair? status) (cdr status)]
[else #f])))
(define/public (prepare-execution debug?)
(set! want-debug? debug?)
(cond
[debug? (send (get-frame) show-debug)]
[else (send (get-frame) hide-debug)
(set! master this)
(for-each (lambda (t) (send t prepare-execution #f)) slaves)
(set! slaves empty)])
(set! current-language-settings (and debug? (send (get-defs) get-next-settings)))
(set! single-step? (box #t))
(set! pos-vec (make-vector (add1 (send (get-defs) last-position)) #f))
(set! top-level-bindings empty)
(set! resume-ch (make-channel))
(set! suspend-sema (make-semaphore 1))
(set! in-user-ch (make-channel))
(set! break-status (box #f))
(set! want-suspend-on-break? #f)
(set! stack-frames (box #f))
(send (get-ints) set-tab this))
(define/augment (on-close)
(inner (void) on-close)
(set-box! closed? #t)
(for-each (lambda (t) (send t prepare-execution #f)) slaves))
(define/public (hide-debug)
(send (get-frame) hide-debug))
(super-new)))
(define debug-bitmap
(make-object bitmap%
(build-path (collection-path "gui-debugger" "icons") "clanbomber-16x16.png")
'png/mask))
(define make-pause-label
(bitmap-label-maker
"Pause"
(build-path (collection-path "gui-debugger" "icons") "pause.png")))
(define make-resume-label
(bitmap-label-maker
"Go"
(build-path (collection-path "gui-debugger" "icons") "resume.png")))
(define make-step-label
(bitmap-label-maker
"Step"
(build-path (collection-path "gui-debugger" "icons") "step.png")))
(define make-over-label
(bitmap-label-maker
"Over"
(build-path (collection-path "gui-debugger" "icons") "step-over2.png")))
(define make-out-label
(bitmap-label-maker
"Out"
(build-path (collection-path "gui-debugger" "icons") "step-out2.png")))
(define (debug-unit-frame-mixin super%)
(class super%
(inherit get-button-panel
get-definitions-text
get-interactions-text
get-menu-bar
get-current-tab
get-top-level-window
get-eventspace)
(define current-language-settings #f)
(define control-panel #f)
(define debug? #f)
(define/public (set-mouse-over-msg msg)
(when (not (string=? msg (send mouse-over-message get-label)))
(send mouse-over-message set-label msg)))
(define/public (debug-callback)
(let ([tab (get-current-tab)])
(cond
[(eq? tab (send tab get-master))
(set! debug? #t)
(execute-callback)
(set! debug? #f)]
[else
(already-debugging tab)])))
(define/override (execute-callback)
(let ([tab (get-current-tab)])
(cond
[(eq? tab (send tab get-master))
(send (get-current-tab) prepare-execution debug?)
(super execute-callback)]
[else
(already-debugging tab)])))
(define/private (already-debugging tab)
(message-box
"Debugger"
(format "This file is involved in a debugging session. To run/debug this file, finish the session for ~a and close or re-run it."
(send (send (send tab get-master) get-defs) get-filename/untitled-name))
this '(ok)))
(define expr-positions empty)
(define expr-lengths empty)
(define/public (register-vars frame)
(send variables-text begin-edit-sequence)
(send variables-text lock #f)
(send variables-text delete 0 (send variables-text last-position))
(for-each
(lambda (name/value)
(let ([name (format "~a" (syntax-e (first name/value)))]
[value (format " => ~s~n" (second name/value))])
(send variables-text insert name)
(send variables-text change-style bold-sd
(- (send variables-text last-position) (string-length name))
(send variables-text last-position))
(send variables-text insert value)
(send variables-text change-style normal-sd
(- (send variables-text last-position) (string-length value))
(send variables-text last-position))))
(third (expose-mark frame)))
(send variables-text lock #t)
(send variables-text end-edit-sequence))
(define/public (register-stack-frames frames already-stopped?)
(let* ([trimmed-exprs
(map (lambda (frame)
(let ([expr (mark-source frame)])
(cond
; should succeed unless the user closes a slave tab during debugging
[(and expr (filename->defs (syntax-source expr)))
=> (lambda (defs)
(trim-expr-str
(if (syntax-position expr)
(send defs get-text
(sub1 (syntax-position expr))
(+ -1 (syntax-position expr) (syntax-span expr)))
"??")
15))]
["??"])))
frames)]
[trimmed-lengths (map add1 (map string-length trimmed-exprs))]
[positions (foldl + 0 trimmed-lengths)])
(send stack-frames begin-edit-sequence)
(send stack-frames lock #f)
(unless already-stopped?
(send stack-frames delete 0 (send stack-frames last-position))
(for-each (lambda (trimmed-expr)
(send stack-frames insert (format "~a~n" trimmed-expr)))
trimmed-exprs))
(send stack-frames change-style normal-sd 0 (send stack-frames last-position))
(send stack-frames change-style bold-sd
(send stack-frames paragraph-start-position (send (get-current-tab) get-frame-num))
(send stack-frames paragraph-end-position (send (get-current-tab) get-frame-num)))
(send stack-frames lock #t)
(send stack-frames end-edit-sequence)))
(define/public (clear-stack-frames/vars)
(send stack-frames begin-edit-sequence)
(send stack-frames lock #f)
(send stack-frames delete 0 (send stack-frames last-position))
(send stack-frames lock #t)
(send stack-frames end-edit-sequence)
(send variables-text begin-edit-sequence)
(send variables-text lock #f)
(send variables-text delete 0 (send variables-text last-position))
(send variables-text lock #t)
(send variables-text end-edit-sequence))
(define debug-grandparent-panel 'uninitialized-debug-grandparent-panel)
(define debug-parent-panel 'uninitialized-debug-parent-panel)
(define debug-panel 'uninitialized-debug-panel)
(define stack-view-panel 'uninitialized-stack-view-panel)
(define stack-frames 'uninitialized-stack-frames)
(define variables-text 'uninitialized-variables-text)
(define highlight-color (make-object color% 207 255 207))
(define bold-sd (make-object style-delta% 'change-weight 'bold))
(define normal-sd (make-object style-delta% 'change-weight 'normal))
(define mouse-over-frame #f)
(define/override (get-definitions/interactions-panel-parent)
(set! debug-grandparent-panel
(make-object horizontal-panel%
(super get-definitions/interactions-panel-parent)))
(set! stack-view-panel
(new panel:vertical-dragable%
[parent debug-grandparent-panel]
[min-width 160]
[stretchable-width #f]))
(set! stack-frames
(new (class text%
(super-new)
(inherit find-line line-start-position line-end-position
change-style begin-edit-sequence end-edit-sequence
lock last-position line-paragraph find-position
dc-location-to-editor-location)
(define highlight-defs #f)
(define highlight-start #f)
(define highlight-end #f)
(define mouse-over-frame #f)
(define/override (on-event evt)
(let*-values ([(x y) (dc-location-to-editor-location
(send evt get-x) (send evt get-y))]
[(line) (find-line y)]
[(pos) (find-position x y)]
[(paragraph) (line-paragraph line)]
[(frames) (send (get-current-tab) get-stack-frames)]
[(frame) (and frames
(> (length frames) paragraph)
(list-ref frames paragraph))]
[(expr) (and frame (mark-source frame))])
(case (send evt get-event-type)
[(enter motion)
(when (and mouse-over-frame (not (= paragraph mouse-over-frame)))
;; motion to different frame: unhighlight old
(send highlight-defs unhighlight-range
highlight-start highlight-end highlight-color)
(set! mouse-over-frame #f))
(when (and expr (not (eq? mouse-over-frame paragraph)))
;; motion to frame: highlight new
(cond
[(filename->defs (syntax-source expr))
=> (lambda (defs)
(set! mouse-over-frame paragraph)
(set! highlight-defs defs)
(set! highlight-start (sub1 (syntax-position expr)))
(set! highlight-end (+ -1 (syntax-position expr)
(syntax-span expr)))
(send defs highlight-range
highlight-start highlight-end highlight-color)
(cond
[(send defs get-filename)
=> (lambda (fn) (handler:edit-file fn))])
(send defs scroll-to-position (syntax-position expr)))]))]
[(leave)
(when mouse-over-frame
;; motion to different frame: unhighlight old
(send highlight-defs unhighlight-range
highlight-start highlight-end highlight-color)
(set! mouse-over-frame #f))
(cond
[(send (get-current-tab) get-frame-num)
=> (lambda (num) (send (get-current-tab) move-to-frame num))])]
[(left-down)
(when (and paragraph expr)
(send (get-current-tab) move-to-frame paragraph))]
[else (void)]))))))
(set! variables-text (new text% [auto-wrap #t]))
(let ([stack-frames-panel (make-object vertical-panel% stack-view-panel)])
(new message% [parent stack-frames-panel] [label "Stack"])
(new editor-canvas% [parent stack-frames-panel] [editor stack-frames] [style '(no-hscroll)]))
(let ([variables-panel (make-object vertical-panel% stack-view-panel)])
(new message% [parent variables-panel] [label "Variables"])
(new editor-canvas% [parent variables-panel] [editor variables-text] [style '(no-hscroll)]))
;; parent of panel with debug buttons
(set! debug-parent-panel
(make-object vertical-panel% debug-grandparent-panel))
;; horizontal panel with debug buttons; not vertically stretchable
(set! debug-panel (instantiate horizontal-panel% ()
(parent debug-parent-panel)
(stretchable-height #f)
(alignment '(center center))
(style '(border))))
;; hide the debug panel and stack view initially
(send debug-parent-panel change-children (lambda (l) null))
(send debug-grandparent-panel change-children (lambda (l) (remq stack-view-panel l)))
(make-object vertical-panel% debug-parent-panel))
(define/public (hide-debug)
(when (member debug-panel (send debug-parent-panel get-children))
(send debug-grandparent-panel change-children
(lambda (l) (remq stack-view-panel l)))
(send debug-parent-panel change-children
(lambda (l) (remq debug-panel l)))))
(define/public (show-debug)
(unless (member debug-panel (send debug-parent-panel get-children))
(send debug-grandparent-panel change-children
(lambda (l) (append l (list stack-view-panel))))
(send debug-parent-panel change-children
(lambda (l) (cons debug-panel l)))))
(super-new)
(define status-message
(instantiate message% ()
[label " "]
[parent debug-panel]
[stretchable-width #t]))
(define debug-button
(new switchable-button%
(label (string-constant debug-tool-button-name))
(bitmap debug-bitmap)
(parent (new vertical-pane%
[parent (get-button-panel)]
[alignment '(center center)]))
(callback (λ (button) (debug-callback)))))
(inherit register-toolbar-button)
(register-toolbar-button debug-button)
(define/augment (enable-evaluation)
(send debug-button enable #t)
(inner (void) enable-evaluation))
(define/augment (disable-evaluation)
(send debug-button enable #f)
(inner (void) disable-evaluation))
(define pause-button
(instantiate button% ()
[label (make-pause-label this)]
[parent debug-panel]
[callback (lambda (button evt)
(cond [(send (get-current-tab) get-stack-frames) (bell)]
[else (send (get-current-tab) suspend-on-break? #t)
(send (get-current-tab) break-callback)
(send (get-current-tab) reset-offer-kill)]))]
[enabled #t]))
(define resume-button
(instantiate button% ()
[label (make-resume-label this)]
[parent debug-panel]
[callback (lambda (button evt)
(if (send (get-current-tab) get-stack-frames)
(send (get-current-tab) resume)
(bell)))]
[enabled #f]))
(define step-button
(instantiate button% ()
[label (make-step-label this)]
[parent debug-panel]
[callback (lambda (btn evt)
(cond [(send (get-current-tab) get-stack-frames)
(send (get-current-tab) set-single-step?! #t)
(send (get-current-tab) resume)]
[else (bell)]))]
[enabled #f]))
(define (make-big-step-callback out?)
(lambda (btn evt)
; go through stack frames until it's possible to set a breakpoint at the end
(let* ([frames (list-tail (send (get-current-tab) get-stack-frames)
(send (get-current-tab) get-frame-num))]
[frames (case (send (get-current-tab) get-break-status)
[(entry-break) (if out? (rest frames) frames)]
[else (if out? (rest frames) empty)])]
[frame (ormap (lambda (f depth)
(let/ec k
(let* ([stx (mark-source f)]
[src (syntax-source stx)]
[lpos (or (syntax-position stx) (k #f))]
[pos (+ lpos (syntax-span stx) -1)]
[defs (filename->defs src)]
[tab (if defs (send defs get-tab) (k #f))]
[bps (send tab get-breakpoints)]
[cur-stat (hash-table-get bps pos 'invalid)])
(case cur-stat
[(invalid) #f]
[else
(hash-table-put!
bps pos
(lambda ()
(and (< (length (continuation-mark-set->list
(current-continuation-marks) debug-key)) depth)
(begin
(hash-table-put! bps pos cur-stat)
#t))))
f]))))
frames
(let ([len (length frames)])
(build-list len (lambda (i) (- len i)))))])
(cond [frames (send (get-current-tab) set-single-step?! (not frame))
(send (get-current-tab) resume)]
[else (bell)]))))
(define step-over-button
(new button%
[label (make-over-label this)]
[parent debug-panel]
[callback (make-big-step-callback #f)]
[enabled #f]))
(define step-out-button
(new button%
[label (make-out-label this)]
[parent debug-panel]
[callback (make-big-step-callback #t)]
[enabled #f]))
(define/public (get-debug-button) debug-button)
(define/public (get-pause-button) pause-button)
(define/public (get-resume-button) resume-button)
(define/public (get-step-button) step-button)
(define/public (get-step-over-button) step-over-button)
(define/public (get-step-out-button) step-out-button)
(define/public (get-status-message) status-message)
(define mouse-over-message
(instantiate message% ()
[label " "] [parent debug-panel] [stretchable-width #t]))
(define/augment (on-tab-change old new)
(check-current-language-for-debugger)
(if (send new debug?)
(let ([status (send new get-break-status)])
(if status
(send new suspend-gui (send new get-stack-frames) status #f #t)
(send new resume-gui))
(show-debug))
(hide-debug))
(inner (void) on-tab-change old new))
(define/public (check-current-language-for-debugger)
(let* ([settings (send (get-definitions-text) get-next-settings)]
[lang (drscheme:language-configuration:language-settings-language settings)]
[visible? (and (send lang capability-value 'gui-debugger:debug-button)
(not (debugger-does-not-work-for?
(extract-language-level settings))))])
(if visible?
(unless (send debug-button is-shown?)
(send (send debug-button get-parent) add-child debug-button))
(when (send debug-button is-shown?)
(send (send debug-button get-parent) delete-child debug-button)))))
(send (get-button-panel) change-children
(lambda (_)
(cons (send debug-button get-parent)
(remq (send debug-button get-parent) _))))
; hide debug button if it's not supported for the initial language:
(check-current-language-for-debugger)))
(drscheme:language:register-capability 'gui-debugger:debug-button (flat-contract boolean?) #t)
(drscheme:get/extend:extend-definitions-text debug-definitions-text-mixin)
(drscheme:get/extend:extend-interactions-text debug-interactions-text-mixin)
(drscheme:get/extend:extend-unit-frame debug-unit-frame-mixin)
(drscheme:get/extend:extend-tab debug-tab-mixin))))