#lang scheme/gui
(require drscheme/tool
framework
embedded-gui srfi/2 scheme/runtime-path
mzlib/unit
)
(define-runtime-path ICON-PATH "expr.png")
(define-runtime-path ICON2-PATH "unbalanced-expr.png")
(provide tool@)
(define tool@
(unit
(import drscheme:tool^)
(export drscheme:tool-exports^)
(define (phase1)
(void))
(define (phase2)
(void))
(define annotate-item-label-string "Annotate this Code")
(define insert-annotated-expression-label-string
"Insert Annotated Code Box")
(define ICON (make-object bitmap% ICON-PATH))
(define UNBALANCED-ICON (make-object bitmap% ICON2-PATH))
(define MIN-TEXT-WIDTH 50)
(define anno-snipclass%
(class decorated-editor-snipclass%
(define/override (make-snip stream-in) (new anno-snip%))
(define/override (read in)
(let* ([s (new anno-snip%)]
[pb (send s get-editor)])
(let ([code-ed (send pb get-code-editor)]
[anno-ed (send pb get-anno-editor)]
[show-code? (box -1)])
(send in get show-code?) (send code-ed read-from-file in 'start)
(send anno-ed read-from-file in 'start)
(send pb show-code (= (unbox show-code?) 1))
s)))
(super-new)))
(define snipclass (make-object anno-snipclass%))
(send snipclass set-version 1)
(send snipclass set-classname
(format "~s" '(lib annotated-snip/annotated-code-tool)))
(send (get-the-snip-class-list) add snipclass)
(define anno-snip%
(class* decorated-editor-snip% (readable-snip<%>)
(inherit get-editor)
(define/override (make-editor) (new anno-pb%))
(define/override (make-snip) (new anno-snip%))
(define/override (get-corner-bitmap)
(or (and-let* ([pb (get-editor)]
[code (and (pb . is-a? . anno-pb%)
(send pb get-code-editor))])
(and (send code contains-balanced-sexp?)
ICON))
UNBALANCED-ICON))
(define/override (get-position) 'left-top)
(define/override (get-text offset num [flattened? #t])
(if flattened?
(let ([pb (get-editor)])
(let ([code (send (send pb get-code-editor)
get-text 0)]
[anno (send (send pb get-anno-editor)
get-text 0)])
(if (string=? anno "")
code
(let ([commented-anno (regexp-replace* "\n" anno "\n; ")])
(if (char=? #\newline
(string-ref commented-anno
(- (string-length commented-anno) 1)))
(string-append code "\n; " commented-anno)
(string-append code "\n; " commented-anno "\n"))))))
"."))
(define/override (get-menu)
(let ([menu (make-object popup-menu%)])
(define (copy-callback perform-copy)
(λ (_ __)
(let ([to-ed (find-containing-editor)])
(when to-ed
(let ([this-pos (find-this-position)])
(when this-pos
(let* ([from-ed (get-editor)]
[from-code (send from-ed get-code-editor)]
[from-anno (send from-ed get-anno-editor)])
(send to-ed begin-edit-sequence)
(send from-ed begin-edit-sequence)
(perform-copy to-ed from-code from-anno (+ this-pos 1))
(send to-ed delete this-pos (+ this-pos 1))
(send to-ed end-edit-sequence)
(send from-ed end-edit-sequence))))))))
(make-show/hide-code-item (get-editor) menu)
(make-object menu-item%
"convert to commented text"
menu
(copy-callback (λ (to-ed from-code from-anno dest-pos)
(if (newline-after-this?)
(copy-contents-with-semicolons-to-position
to-ed from-anno dest-pos)
(begin (send to-ed insert "|#" dest-pos)
(copy-contents-to-position
to-ed from-anno dest-pos)
(send to-ed insert "#|" dest-pos)))
(send to-ed insert #\space dest-pos)
(copy-contents-to-position
to-ed from-code dest-pos))))
(make-object menu-item%
"convert to un-annotated text"
menu
(copy-callback
(λ (to-ed from-code from-anno dest-pos)
(copy-contents-to-position to-ed from-code dest-pos))))
menu))
(inherit get-flags)
(define (newline-after-this?)
(let ([flags (get-flags)])
(or (memq 'hard-newline flags) (memq 'newline flags))))
(inherit get-admin)
(define/private (find-containing-editor)
(let ([admin (get-admin)])
(and admin
(send admin get-editor))))
(define/private (find-this-position)
(let ([ed (find-containing-editor)])
(and ed
(send ed get-snip-position this))))
(define/public (read-special file line col pos)
(let* ([ip (open-input-text-editor (send (get-editor) get-code-editor))]
[expr (read ip)])
(close-input-port ip)
(datum->syntax #f expr (list file line col pos 1))))
(define/override (write stream-out)
(let ([pb (get-editor)])
(let ([code (send pb get-code-editor)]
[anno (send pb get-anno-editor)])
(send stream-out put (if (send pb showing-code?) 1 0))
(send code write-to-file stream-out)
(send anno write-to-file stream-out))))
(define/override (copy)
(let* ([s (new anno-snip%)]
[pb (send (get-editor) copy-self)])
(send s set-editor pb)
s))
(super-new)
(inherit set-snipclass)
(set-snipclass snipclass)
))
(define caches-balanced<%>
(interface ((class->interface scheme:text%))
contains-balanced-sexp?))
(define anno-pb%
(let ()
(define (find-owner-snip ed) (let ([admin (send ed get-admin)])
(and admin
(admin . is-a? . editor-snip-editor-admin<%>)
(send admin get-snip))))
(define scheme+copy-self% (vector #f #f))
(define (get-scheme+copy-self% code-editor?)
(define ce-bit (if code-editor? 1 0))
(unless (vector-ref scheme+copy-self% ce-bit)
(vector-set! scheme+copy-self% ce-bit
(class* (let ([% (tabbable-text-mixin scheme:text%)])
(if code-editor?
((drscheme:unit:get-program-editor-mixin) %)
%))
(caches-balanced<%>)
(define currently-balanced-expr #f)
(inherit copy-self-to)
(define/override (copy-self)
(let ([ed (new scheme+copy-self%)]) (copy-self-to ed) ed))
(define/augment (after-edit-sequence)
(set! currently-balanced-expr
(scheme:text-balanced? this))
(inner (void) after-edit-sequence))
(define/public (contains-balanced-sexp?) currently-balanced-expr)
(super-new)
(inherit set-max-undo-history)
(set-max-undo-history 'forever))))
(vector-ref scheme+copy-self% ce-bit))
(class aligned-pasteboard%
(super-new)
(define/public (get-code-editor) code-editor)
(define/public (make-code-editor)
(new (get-scheme+copy-self% #t)))
(define/public (get-anno-editor) anno-editor)
(define/public (make-anno-editor)
(new (get-scheme+copy-self% #f)))
(define code-editor (make-code-editor)) (define anno-editor (make-anno-editor)) (set-tabbing code-editor anno-editor)
(define val (new vertical-alignment% [parent this]))
(define code-view (new vertical-alignment%
[parent val]
[show? #f]))
(define (init-snips)
(if (not (or (find-owner-snip code-editor)
(find-owner-snip anno-editor)))
(let ([code-snip (new stretchable-editor-snip%
[editor code-editor]
[with-border? #t]
[min-width MIN-TEXT-WIDTH])]
[anno-snip (new stretchable-editor-snip%
[editor anno-editor]
[with-border? #f]
[min-width MIN-TEXT-WIDTH])])
(make-object snip-wrapper% code-view code-snip)
(make-object hline% code-view)
(make-object snip-wrapper% val anno-snip)
#t)
#f))
(init-snips)
(define showing-code #f)
(define/public (showing-code?) showing-code)
(define/public (show-code on/off) (send code-view show on/off)
(and-let* ([ed (get-code-editor)]
[snip (find-owner-snip ed)]
[adm (send snip get-admin)])
(send adm set-caret-owner snip 'global))
(set! showing-code on/off))
(define/override (copy-self) (let ([ed (new anno-pb%)])
(let ([ed-code (send ed get-code-editor)]
[ed-anno (send ed get-anno-editor)])
(send (get-code-editor) copy-self-to ed-code)
(send (get-anno-editor) copy-self-to ed-anno)
(send ed show-code (showing-code?))
ed)))
)))
(define (make-show/hide-code-item pb menu)
(let ([showing-code-now? (send pb showing-code?)])
(new menu-item%
[label (if showing-code-now?
"Hide program text"
"Show program text")]
[parent menu]
[callback (lambda (item evt)
(send pb show-code (not showing-code-now?)))])))
(define (copy-contents-with-semicolons-to-position to-ed from-ed pos)
(let loop ([snip (find-last-snip from-ed)])
(cond
[snip
(when (or (memq 'hard-newline (send snip get-flags))
(memq 'newline (send snip get-flags)))
(send to-ed insert "; " pos))
(send to-ed insert (send snip copy) pos)
(loop (send snip previous))]
[else
(send to-ed insert "; " pos)])))
(define (copy-contents-to-position to-ed from-ed pos)
(let loop ([snip (find-last-snip from-ed)])
(when snip
(send to-ed insert (send snip copy) pos)
(loop (send snip previous)))))
(define (find-last-snip ed)
(let loop ([snip (send ed find-first-snip)]
[acc (send ed find-first-snip)])
(cond
[snip (loop (send snip next) snip)]
[else acc])))
(define add-annotate-to-menu
(let ()
(define (make-annotate-item text L R menu)
(new separator-menu-item% [parent menu])
(new menu-item%
[parent menu]
[label annotate-item-label-string]
[callback (λ (item evt)
(convert-to-annotated-sexp text L R))]))
(lambda (menu text event)
(when (and (not (send text is-frozen?))
(not (send text is-stopped?)))
(let* ([on-it-box (box #f)]
[click-pos
(call-with-values
(λ ()
(send text dc-location-to-editor-location
(send event get-x)
(send event get-y)))
(λ (x y)
(send text find-position x y #f on-it-box)))]
[snip (send text find-snip click-pos 'after)]
[char (send text get-character click-pos)]
[left? (memq char '(#\( #\{ #\[))]
[right? (memq char '(#\) #\} #\]))])
(cond
[(not (unbox on-it-box))
(void)]
[(or left? right?)
(let* ([pos (if left?
click-pos
(+ click-pos 1))]
[other-pos (if left?
(send text get-forward-sexp pos)
(send text get-backward-sexp pos))])
(when other-pos
(let ([left-pos (min pos other-pos)]
[right-pos (max pos other-pos)])
(make-annotate-item text left-pos right-pos menu))))]
[else
(let ([up-sexp (send text find-up-sexp click-pos)])
(when up-sexp
(let ([fwd (send text get-forward-sexp up-sexp)])
(make-annotate-item text up-sexp fwd menu))))]))))))
(define (convert-to-annotated-sexp text L R)
(send text begin-edit-sequence)
(send text split-snip L)
(send text split-snip R)
(let ([snips (let loop ([snip (send text find-snip L 'after)])
(cond
[(not snip) null]
[((send text get-snip-position snip) . >= . R)
null]
[else (cons (send snip copy) (loop (send snip next)))]))])
(send text delete L R)
(let* ([ann-snip (new anno-snip%)]
[pb (send ann-snip get-editor)]
[code-ed (send pb get-code-editor)])
(for-each (lambda (snip) (send code-ed insert snip)) snips)
(send pb show-code #t)
(send text insert ann-snip L L)
(send text end-edit-sequence)
)))
(keymap:add-to-right-button-menu
(let ([old (keymap:add-to-right-button-menu)])
(λ (menu ed event)
(old menu ed event) (add-annotate-to-menu menu ed event)
(void))))
(define (anno-snip-menu-items-mixin %) (class %
(inherit get-insert-menu
get-language-menu
get-edit-target-object
register-capability-menu-item)
(super-new)
(define find-insertion-point (lambda ()
(let ([editor (get-edit-target-object)])
(and editor
(is-a? editor editor<%>)
(let loop ([editor editor])
(let ([focused (send editor get-focus-snip)])
(if (and focused
(is-a? focused editor-snip%))
(loop (send focused get-editor))
editor)))))))
(define (insert-anno-snip menu evt)
(let ([anno-snip (new anno-snip%)]
[text (find-insertion-point)])
(when text
(send text insert anno-snip)
(send text set-caret-owner anno-snip 'global))))
(define (annotate-current-sexp/selection menu evt)
(let ([text (find-insertion-point)]
[bs (box 0)] [be (box 0)]) (send text get-position bs be)
(let ([start (unbox bs)]
[end (unbox be)])
(if (= start end) (let ([up-sexp (send text find-up-sexp start)])
(when up-sexp
(let ([fwd (send text get-forward-sexp up-sexp)])
(convert-to-annotated-sexp text up-sexp fwd))))
(convert-to-annotated-sexp text start end)))))
(make-object menu-item%
annotate-item-label-string
(get-language-menu)
annotate-current-sexp/selection)
(let ([insert-snip
(lambda (make-obj)
(let ([editor (find-insertion-point)])
(when editor
(let ([snip (make-obj)])
(send editor insert snip)
(send editor set-caret-owner snip 'display)))))]
[demand-callback (lambda (item)
(send item enable (find-insertion-point)))])
(instantiate menu-item% (insert-annotated-expression-label-string
(get-insert-menu)
insert-anno-snip)
[demand-callback demand-callback]))
))
(drscheme:get/extend:extend-unit-frame
anno-snip-menu-items-mixin)
(define test-w/text% (let ()
(define (mk-test-frame)
(new frame% [label "Annotated code snip test"] [width 500] [height 300]
[alignment '(center center)]))
(lambda ()
(define test-snip (new anno-snip%))
(define test-pb (send test-snip get-editor))
(define txt (new scheme:text%))
(define f (mk-test-frame))
(define c (new editor-canvas% [parent f][editor txt]))
(send txt set-max-undo-history 'forever)
(send txt insert "\n(* x 3)\n4))" 0)
(send txt insert test-snip 0)
(send txt insert "(define (f x)\n(+ " 0)
(send txt tabify-all)
(send f show #t)
(values test-snip txt)
)))
))