racket/collects/framework/private/text.ss
2008-04-08 21:42:38 +00:00

2862 lines
110 KiB
Scheme

#|
WARNING: printf is rebound in the body of the unit to always
print to the original output port.
|#
#lang scheme/unit
(require string-constants
mzlib/class
mzlib/match
scheme/path
"sig.ss"
"../gui-utils.ss"
"../preferences.ss"
(lib "mred-sig.ss" "mred")
(lib "interactive-value-port.ss" "mrlib")
mzlib/list
mzlib/etc
setup/dirs
mzlib/string
(prefix-in srfi1: srfi/1))
(require setup/xref
scribble/xref
scribble/struct
scribble/manual-struct
scribble/decode
scribble/basic
(prefix-in s/m: scribble/manual))
(import mred^
[prefix icon: framework:icon^]
[prefix editor: framework:editor^]
[prefix keymap: framework:keymap^]
[prefix color-model: framework:color-model^]
[prefix frame: framework:frame^]
[prefix scheme: framework:scheme^]
[prefix number-snip: framework:number-snip^]
[prefix finder: framework:finder^])
(export (rename framework:text^
[-keymap% keymap%]))
(init-depend framework:editor^)
(define original-output-port (current-output-port))
(define (printf . args)
(apply fprintf original-output-port args)
(void))
(define-struct range (start end b/w-bitmap color caret-space?))
(define-struct rectangle (left top right bottom b/w-bitmap color))
;; wx: `default-wrapping?', add as the initial value for auto-wrap bitmap,
;; unless matthew makes it primitive
(define basic<%>
(interface (editor:basic<%> (class->interface text%))
highlight-range
unhighlight-range
get-highlighted-ranges
get-styles-fixed
get-fixed-style
set-styles-fixed
move/copy-to-edit
initial-autowrap-bitmap
get-port-name
port-name-matches?))
(define basic-mixin
(mixin (editor:basic<%> (class->interface text%)) (basic<%>)
(inherit get-canvas get-canvases get-admin split-snip get-snip-position
begin-edit-sequence end-edit-sequence
set-autowrap-bitmap
delete find-snip invalidate-bitmap-cache
set-file-format get-file-format
get-style-list is-modified? change-style set-modified
position-location get-extent get-filename)
(define port-name-identifier #f)
(define/public (get-port-name)
(let* ([b (box #f)]
[n (get-filename b)])
(cond
[(or (unbox b) (not n))
(unless port-name-identifier
(set! port-name-identifier (gensym 'unsaved-editor)))
port-name-identifier]
[else n])))
(define/public (port-name-matches? id)
(let ([filename (get-filename)])
(or (and (path? id)
(path? filename)
(equal? (normal-case-path (normalize-path (get-filename)))
(normal-case-path (normalize-path id))))
(and (symbol? port-name-identifier)
(symbol? id)
(equal? port-name-identifier id)))))
(define highlight-pen #f)
(define highlight-brush #f)
(define range-rectangles null)
(define ranges null)
(define/public-final (get-highlighted-ranges) ranges)
(define/public (get-fixed-style)
(send (get-style-list) find-named-style "Standard"))
(define/private (invalidate-rectangles rectangles)
(let ([b1 (box 0)]
[b2 (box 0)]
[b3 (box 0)]
[b4 (box 0)]
[canvases (get-canvases)])
(let-values ([(min-left max-right)
(cond
[(null? canvases)
(let ([admin (get-admin)])
(if admin
(begin
(send admin get-view b1 b2 b3 b4)
(let* ([this-left (unbox b1)]
[this-width (unbox b3)]
[this-right (+ this-left this-width)])
(values this-left
this-right)))
(values #f #f)))]
[else
(let loop ([left #f]
[right #f]
[canvases canvases])
(cond
[(null? canvases)
(values left right)]
[else
(let-values ([(this-left this-right)
(send (car canvases)
call-as-primary-owner
(λ ()
(send (get-admin) get-view b1 b2 b3 b4)
(let* ([this-left (unbox b1)]
[this-width (unbox b3)]
[this-right (+ this-left this-width)])
(values this-left
this-right))))])
(if (and left right)
(loop (min this-left left)
(max this-right right)
(cdr canvases))
(loop this-left
this-right
(cdr canvases))))]))])])
(when (and min-left max-right)
(let loop ([left #f]
[top #f]
[right #f]
[bottom #f]
[rectangles rectangles])
(cond
[(null? rectangles)
(when left
(let ([width (- right left)]
[height (- bottom top)])
(when (and (> width 0)
(> height 0))
(invalidate-bitmap-cache left top width height))))]
[else (let* ([r (car rectangles)]
[rleft (rectangle-left r)]
[rright (rectangle-right r)]
[rtop (rectangle-top r)]
[rbottom (rectangle-bottom r)]
[this-left (if (number? rleft)
rleft
min-left)]
[this-right (if (number? rright)
rright
max-right)]
[this-bottom rbottom]
[this-top rtop])
(if (and left top right bottom)
(loop (min this-left left)
(min this-top top)
(max this-right right)
(max this-bottom bottom)
(cdr rectangles))
(loop this-left
this-top
this-right
this-bottom
(cdr rectangles))))]))))))
(define/private (recompute-range-rectangles)
(let* ([b1 (box 0)]
[b2 (box 0)]
[new-rectangles
(λ (range)
(let* ([start (range-start range)]
[end (range-end range)]
[b/w-bitmap (range-b/w-bitmap range)]
[color (range-color range)]
[caret-space? (range-caret-space? range)]
[start-eol? #f]
[end-eol? (if (= start end)
start-eol?
#t)])
(let-values ([(start-x top-start-y)
(begin
(position-location start b1 b2 #t start-eol? #t)
(values (if caret-space?
(+ 1 (unbox b1))
(unbox b1))
(unbox b2)))]
[(end-x top-end-y)
(begin (position-location end b1 b2 #t end-eol? #t)
(values (unbox b1) (unbox b2)))]
[(bottom-start-y)
(begin (position-location start b1 b2 #f start-eol? #t)
(unbox b2))]
[(bottom-end-y)
(begin (position-location end b1 b2 #f end-eol? #t)
(unbox b2))])
(cond
[(= top-start-y top-end-y)
(list
(make-rectangle start-x
top-start-y
(if (= end-x start-x)
(+ end-x 1)
end-x)
bottom-start-y
b/w-bitmap
color))]
[else
(list
(make-rectangle start-x
top-start-y
'right-edge
bottom-start-y
b/w-bitmap
color)
(make-rectangle 'left-edge
bottom-start-y
'max-width
top-end-y
b/w-bitmap
color)
(make-rectangle 'left-edge
top-end-y
end-x
bottom-end-y
b/w-bitmap
color))]))))]
[old-rectangles range-rectangles])
(set! range-rectangles
(foldl (λ (x l) (append (new-rectangles x) l))
null ranges))))
(define/public highlight-range
(opt-lambda (start end color [bitmap #f] [caret-space? #f] [priority 'low])
(unless (let ([exact-pos-int?
(λ (x) (and (integer? x) (exact? x) (x . >= . 0)))])
(and (exact-pos-int? start)
(exact-pos-int? end)))
(error 'highlight-range "expected first two arguments to be non-negative exact integers, got: ~e ~e"
start end))
(unless (or (eq? priority 'high) (eq? priority 'low))
(error 'highlight-range "expected last argument to be either 'high or 'low, got: ~e"
priority))
(unless (is-a? color color%)
(error 'highlight-range "expected a color for the third argument, got ~s" color))
(let ([l (make-range start end bitmap color caret-space?)])
(invalidate-rectangles range-rectangles)
(set! ranges (if (eq? priority 'high) (cons l ranges) (append ranges (list l))))
(recompute-range-rectangles)
(invalidate-rectangles range-rectangles)
(λ () (unhighlight-range start end color bitmap caret-space?)))))
(define/public unhighlight-range
(opt-lambda (start end color [bitmap #f] [caret-space? #f])
(let ([old-rectangles range-rectangles])
(set! ranges
(let loop ([r ranges])
(cond
[(null? r) r]
[else (if (matching-rectangle? (car r) start end color bitmap caret-space?)
(cdr r)
(cons (car r) (loop (cdr r))))])))
(recompute-range-rectangles)
(invalidate-rectangles old-rectangles))))
(define/private (matching-rectangle? r start end color bitmap caret-space?)
(and (equal? start (range-start r))
(equal? end (range-end r))
(eq? bitmap (range-b/w-bitmap r))
(equal? color (range-color r))
(equal? caret-space? (range-caret-space? r))))
(define/override (on-paint before dc left-margin top-margin right-margin bottom-margin dx dy draw-caret)
(super on-paint before dc left-margin top-margin right-margin bottom-margin dx dy draw-caret)
(recompute-range-rectangles)
(let ([b1 (box 0)]
[b2 (box 0)]
[b3 (box 0)]
[b4 (box 0)])
(for-each
(λ (rectangle)
(let-values ([(view-x view-y view-width view-height)
(begin
(send (get-admin) get-view b1 b2 b3 b4)
(values (unbox b1)
(unbox b2)
(unbox b3)
(unbox b4)))])
(let* ([old-pen (send dc get-pen)]
[old-brush (send dc get-brush)]
[b/w-bitmap (rectangle-b/w-bitmap rectangle)]
[color (let* ([rc (rectangle-color rectangle)]
[tmpc (make-object color% 0 0 0)])
(if rc
(begin (send dc try-color rc tmpc)
(if (<= (color-model:rgb-color-distance
(send rc red)
(send rc green)
(send rc blue)
(send tmpc red)
(send tmpc green)
(send tmpc blue))
18)
rc
#f))
rc))]
[first-number (λ (x y) (if (number? x) x y))]
[left (max left-margin (first-number (rectangle-left rectangle) view-x))]
[top (max top-margin (rectangle-top rectangle))]
[right (min right-margin
(first-number
(rectangle-right rectangle)
(+ view-x view-width)))]
[bottom (min bottom-margin (rectangle-bottom rectangle))]
[width (max 0 (- right left))]
[height (max 0 (- bottom top))])
(let/ec k
(cond
[(and before color)
(send dc set-pen (send the-pen-list find-or-create-pen color 0 'solid))
(send dc set-brush (send the-brush-list find-or-create-brush color 'solid))]
[(and (not before) (not color) b/w-bitmap)
(unless highlight-pen
(set! highlight-pen (make-object pen% "BLACK" 0 'solid)))
(unless highlight-brush
(set! highlight-brush (make-object brush% "black" 'solid)))
(send highlight-pen set-stipple b/w-bitmap)
(send highlight-brush set-stipple b/w-bitmap)
(send dc set-pen highlight-pen)
(send dc set-brush highlight-brush)]
[else (k (void))])
(send dc draw-rectangle (+ left dx) (+ top dy) width height)
(send dc set-pen old-pen)
(send dc set-brush old-brush)))))
range-rectangles)))
(define styles-fixed? #f)
(public get-styles-fixed set-styles-fixed)
(define (get-styles-fixed) styles-fixed?)
(define (set-styles-fixed b) (set! styles-fixed? b))
(define/augment (on-insert start len)
(begin-edit-sequence)
(inner (void) on-insert start len))
(define/augment (after-insert start len)
(when styles-fixed?
(change-style (get-fixed-style) start (+ start len) #f))
(inner (void) after-insert start len)
(end-edit-sequence))
(public move/copy-to-edit)
(define (move/copy-to-edit dest-edit start end dest-position)
(split-snip start)
(split-snip end)
(let loop ([snip (find-snip end 'before)])
(cond
[(or (not snip) (< (get-snip-position snip) start))
(void)]
[else
(let ([prev (send snip previous)]
[released/copied (if (send snip release-from-owner)
snip
(let* ([copy (send snip copy)]
[snip-start (get-snip-position snip)]
[snip-end (+ snip-start (send snip get-count))])
(delete snip-start snip-end)
snip))])
(send dest-edit insert released/copied dest-position dest-position)
(loop prev))])))
(public initial-autowrap-bitmap)
(define (initial-autowrap-bitmap) (icon:get-autowrap-bitmap))
(define/override (put-file directory default-name)
(let* ([canvas (get-canvas)]
[parent (and canvas (send canvas get-top-level-window))])
(finder:put-file default-name
directory
#f
(string-constant select-file)
#f
""
parent)))
(super-new)
(set-autowrap-bitmap (initial-autowrap-bitmap))))
(define foreground-color<%>
(interface (basic<%> editor:standard-style-list<%>)
))
(define foreground-color-mixin
(mixin (basic<%> editor:standard-style-list<%>) (foreground-color<%>)
(inherit begin-edit-sequence end-edit-sequence change-style get-style-list)
(define/override (default-style-name)
(editor:get-default-color-style-name))
(define/override (get-fixed-style)
(send (editor:get-standard-style-list)
find-named-style
(editor:get-default-color-style-name)))
(super-new)))
(define hide-caret/selection<%> (interface (basic<%>)))
(define hide-caret/selection-mixin
(mixin (basic<%>) (hide-caret/selection<%>)
(inherit get-start-position get-end-position hide-caret)
(define/augment (after-set-position)
(hide-caret (= (get-start-position) (get-end-position)))
(inner (void) after-set-position))
(super-new)))
(define nbsp->space<%> (interface ((class->interface text%))))
(define nbsp->space-mixin
(mixin ((class->interface text%)) (nbsp->space<%>)
(field [rewriting #f])
(inherit begin-edit-sequence end-edit-sequence delete insert get-character)
(define/augment (on-insert start len)
(inner (void) on-insert start len)
(begin-edit-sequence))
(inherit find-string)
(define/augment (after-insert start len)
(unless rewriting
(set! rewriting #t)
(let ([str (string (integer->char 160))]
[last-pos (+ start len)])
(let loop ([pos start])
(when (< pos last-pos)
(let ([next-pos (find-string str 'forward pos last-pos)])
(when next-pos
(delete next-pos (+ next-pos 1) #f)
(insert " " next-pos next-pos #f)
(loop (+ next-pos 1)))))))
(set! rewriting #f))
(end-edit-sequence)
(inner (void) after-insert start len))
(super-instantiate ())))
(define searching<%> (interface (editor:keymap<%> basic<%>)))
(define searching-mixin
(mixin (editor:keymap<%> basic<%>) (searching<%>)
(define/override (get-keymaps)
(cons (keymap:get-search) (super get-keymaps)))
(super-instantiate ())))
(define return<%> (interface ((class->interface text%))))
(define return-mixin
(mixin ((class->interface text%)) (return<%>)
(init-field return)
(define/override (on-local-char key)
(let ([cr-code #\return]
[lf-code #\newline]
[code (send key get-key-code)])
(or (and (char? code)
(or (char=? lf-code code)
(char=? cr-code code))
(return))
(super on-local-char key))))
(super-new)))
(define wide-snip<%>
(interface (basic<%>)
add-wide-snip
add-tall-snip))
(define wide-snip-mixin
(mixin (basic<%>) (wide-snip<%>)
(define wide-snips '())
(define tall-snips '())
(define/public (add-wide-snip s) (set! wide-snips (cons s wide-snips)))
(define/public (get-wide-snips) wide-snips)
(define/public (add-tall-snip s) (set! tall-snips (cons s tall-snips)))
(define/public (get-tall-snips) tall-snips)
(super-new)))
(define delegate<%> (interface (basic<%>)
get-delegate
set-delegate))
(define small-version-of-snip%
(class snip%
(init-field big-snip)
(define width 0)
(define height 0)
(define/override (get-extent dc x y wb hb db sb lb rb)
(set/f! db 0)
(set/f! sb 0)
(set/f! lb 0)
(set/f! rb 0)
(let ([bwb (box 0)]
[bhb (box 0)])
(send big-snip get-extent dc x y bwb bhb #f #f #f #f)
(let* ([cw (send dc get-char-width)]
[ch (send dc get-char-height)]
[w (floor (/ (unbox bwb) cw))]
[h (floor (/ (unbox bhb) ch))])
(set/f! wb w)
(set/f! hb h)
(set! width w)
(set! height h))))
(define/override (draw dc x y left top right bottom dx dy draw-caret)
(send dc draw-rectangle x y width height))
(define/override (copy) (instantiate small-version-of-snip% () (big-snip big-snip)))
(super-instantiate ())))
(define 1-pixel-string-snip%
(class string-snip%
(init-rest args)
(inherit get-text get-count set-count get-flags)
(define/override (split position first second)
(let* ([str (get-text 0 (get-count))]
[new-second (make-object 1-pixel-string-snip%
(substring str position (string-length str)))])
(set-box! first this)
(set-box! second new-second)
(set-count position)
(void)))
(define/override (copy)
(let ([cpy (make-object 1-pixel-string-snip%
(get-text 0 (get-count)))])
(send cpy set-flags (get-flags))))
(define/override (get-extent dc x y wb hb db sb lb rb)
(cond
[(memq 'invisible (get-flags))
(set/f! wb 0)]
[else
(set/f! wb (get-count))])
(set/f! hb 1)
(set/f! db 0)
(set/f! sb 0)
(set/f! lb 0)
(set/f! rb 0))
(define cache-function #f)
(define/override (insert s len pos)
(set! cache-function #f)
(super insert s len pos))
;; for-each/sections : string -> dc number number -> void
(define/private (for-each/sections str)
(let loop ([n (string-length str)]
[len 0]
[blank? #t])
(cond
[(zero? n)
(if blank?
(λ (dc x y) (void))
(λ (dc x y)
(send dc draw-line (+ x n) y (+ x n (- len 1)) y)))]
[else
(let ([white? (char-whitespace? (string-ref str (- n 1)))])
(cond
[(eq? white? blank?)
(loop (- n 1) (+ len 1) blank?)]
[else
(let ([res (loop (- n 1) 1 (not blank?))])
(if blank?
res
(λ (dc x y)
(send dc draw-line (+ x n) y (+ x n (- len 1)) y)
(res dc x y))))]))])))
(define/override (draw dc x y left top right bottom dx dy draw-caret)
(let ([str (get-text 0 (get-count))])
(unless cache-function
(set! cache-function (for-each/sections str)))
(when (<= top y bottom)
(cache-function dc x y))))
(apply super-make-object args)))
(define 1-pixel-tab-snip%
(class tab-snip%
(init-rest args)
(inherit get-text get-count set-count get-flags)
(define/override (split position first second)
(let* ([str (get-text 0 (get-count))]
[new-second (make-object 1-pixel-string-snip%
(substring str position (string-length str)))])
(set-box! first this)
(set-box! second new-second)
(set-count position)
(void)))
(define/override (copy)
(let ([cpy (make-object 1-pixel-tab-snip%)])
(send cpy set-flags (get-flags))))
(inherit get-admin)
(define/override (get-extent dc x y wb hb db sb lb rb)
(set/f! wb 0)
(let ([admin (get-admin)])
(when admin
(let ([ed (send admin get-editor)])
(when (is-a? ed text%)
(let ([len-b (box 0)]
[tab-width-b (box 0)]
[in-units-b (box #f)])
(send ed get-tabs len-b tab-width-b in-units-b)
(when (and (or (equal? (unbox len-b) 0)
(equal? (unbox len-b) null))
(not (unbox in-units-b)))
(let ([tabspace (unbox tab-width-b)])
(set/f! wb (tabspace . - . (x . modulo . tabspace))))))))))
(set/f! hb 0)
(set/f! db 0)
(set/f! sb 0)
(set/f! lb 0)
(set/f! rb 0))
(define/override (draw dc x y left top right bottom dx dy draw-caret)
(void))
(apply super-make-object args)))
(define (set/f! b n)
(when (box? b)
(set-box! b n)))
(define delegate-mixin
(mixin (basic<%>) (delegate<%>)
(inherit split-snip find-snip get-snip-position
find-first-snip get-style-list set-tabs)
(define linked-snips #f)
(define/private (copy snip)
(let ([new-snip
(cond
[(is-a? snip tab-snip%)
(let ([new-snip (make-object 1-pixel-tab-snip%)])
(send new-snip insert (string #\tab) 1)
new-snip)]
[(is-a? snip string-snip%)
(make-object 1-pixel-string-snip%
(send snip get-text 0 (send snip get-count)))]
[else
(let ([new-snip
(instantiate small-version-of-snip% ()
(big-snip snip))])
(hash-set! linked-snips snip new-snip)
new-snip)])])
(send new-snip set-flags (send snip get-flags))
(send new-snip set-style (send snip get-style))
new-snip))
(define delegate #f)
(inherit get-highlighted-ranges)
(define/public-final (get-delegate) delegate)
(define/public-final (set-delegate _d)
(set! delegate _d)
(set! linked-snips (if _d
(make-hasheq)
#f))
(refresh-delegate))
(define/private (refresh-delegate)
(when delegate
(send delegate begin-edit-sequence)
(send delegate lock #f)
(when (is-a? this scheme:text<%>)
(send delegate set-tabs null (send this get-tab-size) #f))
(send delegate hide-caret #t)
(send delegate erase)
(send delegate set-style-list (get-style-list))
(let loop ([snip (find-first-snip)])
(when snip
(let ([copy-of-snip (copy snip)])
(send delegate insert
copy-of-snip
(send delegate last-position)
(send delegate last-position))
(loop (send snip next)))))
(for-each
(λ (range)
(send delegate unhighlight-range
(range-start range)
(range-end range)
(range-color range)
(range-b/w-bitmap range)
(range-caret-space? range)))
(send delegate get-highlighted-ranges))
(for-each
(λ (range)
(send delegate highlight-range
(range-start range)
(range-end range)
(range-color range)
(range-b/w-bitmap range)
(range-caret-space? range)
'high))
(reverse (get-highlighted-ranges)))
(send delegate lock #t)
(send delegate end-edit-sequence)))
(define/override highlight-range
(opt-lambda (start end color [bitmap #f] [caret-space? #f] [priority 'low])
(when delegate
(send delegate highlight-range
start end color bitmap caret-space? priority))
(super highlight-range start end color bitmap caret-space? priority)))
(define/override unhighlight-range
(opt-lambda (start end color [bitmap #f] [caret-space? #f])
(when delegate
(send delegate unhighlight-range start end color bitmap caret-space?))
(super unhighlight-range start end color bitmap caret-space?)))
(inherit get-canvases get-active-canvas has-focus?)
(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?)
(unless before?
(let ([active-canvas (get-active-canvas)])
(when active-canvas
(send (send active-canvas get-top-level-window) delegate-moved)))))
(define/augment (on-edit-sequence)
(when delegate
(send delegate begin-edit-sequence))
(inner (void) on-edit-sequence))
(define/augment (after-edit-sequence)
(when delegate
(send delegate end-edit-sequence))
(inner (void) after-edit-sequence))
(define/override (resized snip redraw-now?)
(super resized snip redraw-now?)
(when (and delegate
linked-snips
(not (is-a? snip string-snip%)))
(let ([delegate-copy (hash-ref linked-snips snip (λ () #f))])
(when delegate-copy
(send delegate resized delegate-copy redraw-now?)))))
(define/augment (after-insert start len)
(when delegate
(send delegate begin-edit-sequence)
(send delegate lock #f)
(split-snip start)
(split-snip (+ start len))
(let loop ([snip (find-snip (+ start len) 'before)])
(when snip
(unless ((get-snip-position snip) . < . start)
(send delegate insert (copy snip) start start)
(loop (send snip previous)))))
(send delegate lock #t)
(send delegate end-edit-sequence))
(inner (void) after-insert start len))
(define/augment (after-delete start len)
(when delegate
(send delegate lock #f)
(send delegate begin-edit-sequence)
(send delegate delete start (+ start len))
(send delegate end-edit-sequence)
(send delegate lock #t))
(inner (void) after-delete start len))
(define/augment (after-change-style start len)
(when delegate
(send delegate begin-edit-sequence)
(send delegate lock #f)
(split-snip start)
(let* ([snip (find-snip start 'after)]
[style (send snip get-style)]
[other-style
'(send (send delegate get-style-list) find-or-create-style
style delegate-style-delta)])
(send delegate change-style style start (+ start len)))
(send delegate lock #f)
(send delegate end-edit-sequence))
(inner (void) after-change-style start len))
(define filename #f)
(define format #f)
(define/augment (on-load-file _filename _format)
(set! filename _filename)
(set! format _format)
(inner (void) on-load-file _filename _format))
(define/augment (after-load-file success?)
(when success?
(refresh-delegate))
(inner (void) after-load-file success?))
(super-instantiate ())))
(define info<%> (interface (basic<%>)))
(define info-mixin
(mixin (editor:keymap<%> basic<%>) (info<%>)
(inherit get-start-position get-end-position get-canvas
run-after-edit-sequence)
(define/private (enqueue-for-frame call-method tag)
(run-after-edit-sequence
(rec from-enqueue-for-frame
(λ ()
(call-with-frame call-method)))
tag))
;; call-with-frame : ((is-a?/c frame:text-info<%>) -> void) -> void
;; calls the argument thunk with the frame showing this editor.
(define/private (call-with-frame call-method)
(let ([canvas (get-canvas)])
(when canvas
(let ([frame (send canvas get-top-level-window)])
(when (is-a? frame frame:text-info<%>)
(call-method frame))))))
(define/override (set-anchor x)
(super set-anchor x)
(enqueue-for-frame
(λ (x) (send x anchor-status-changed))
'framework:anchor-status-changed))
(define/override (set-overwrite-mode x)
(super set-overwrite-mode x)
(enqueue-for-frame
(λ (x) (send x overwrite-status-changed))
'framework:overwrite-status-changed))
(define/augment (after-set-position)
(maybe-queue-editor-position-update)
(inner (void) after-set-position))
;; maybe-queue-editor-position-update : -> void
;; updates the editor-position in the frame,
;; but delays it until the next low-priority event occurs.
(define callback-running? #f)
(define/private (maybe-queue-editor-position-update)
(enqueue-for-frame
(λ (frame)
(unless callback-running?
(set! callback-running? #t)
(queue-callback
(λ ()
(send frame editor-position-changed)
(set! callback-running? #f))
#f)))
'framework:info-frame:update-editor-position))
(define/augment (after-insert start len)
(maybe-queue-editor-position-update)
(inner (void) after-insert start len))
(define/augment (after-delete start len)
(maybe-queue-editor-position-update)
(inner (void) after-delete start len))
(super-new)))
(define clever-file-format<%> (interface ((class->interface text%))))
(define clever-file-format-mixin
(mixin ((class->interface text%)) (clever-file-format<%>)
(inherit get-file-format set-file-format find-first-snip)
(define/private (all-string-snips)
(let loop ([s (find-first-snip)])
(cond
[(not s) #t]
[(is-a? s string-snip%)
(loop (send s next))]
[else #f])))
(define/augment (on-save-file name format)
(let ([all-strings? (all-string-snips)])
(cond
[(and all-strings?
(eq? format 'same)
(eq? 'standard (get-file-format))
(or (not (preferences:get 'framework:verify-change-format))
(gui-utils:get-choice
(string-constant save-as-plain-text)
(string-constant yes)
(string-constant no))))
(set-file-format 'text)]
[(and (not all-strings?)
(eq? format 'same)
(eq? 'text (get-file-format))
(or (not (preferences:get 'framework:verify-change-format))
(gui-utils:get-choice
(string-constant save-in-drs-format)
(string-constant yes)
(string-constant no))))
(set-file-format 'standard)]
[else (void)]))
(inner (void) on-save-file name format))
(super-instantiate ())))
(define file<%>
(interface (editor:file<%> basic<%>)
get-read-write?
while-unlocked))
(define file-mixin
(mixin (editor:file<%> basic<%>) (file<%>)
(inherit get-filename)
(define read-write? #t)
(define/public (get-read-write?) read-write?)
(define/private (check-lock)
(let* ([filename (get-filename)]
[can-edit? (if (and filename
(file-exists? filename))
(and (member 'write (file-or-directory-permissions filename))
#t)
#t)])
(set! read-write? can-edit?)))
(define/public (while-unlocked t)
(let ([unlocked? 'unint])
(dynamic-wind
(λ ()
(set! unlocked? read-write?)
(set! read-write? #t))
(λ () (t))
(λ () (set! read-write? unlocked?)))))
(define/augment (can-insert? x y)
(and read-write? (inner #t can-insert? x y)))
(define/augment (can-delete? x y)
(and read-write? (inner #t can-delete? x y)))
(define/augment (after-save-file success)
(when success
(check-lock))
(inner (void) after-save-file success))
(define/augment (after-load-file sucessful?)
(when sucessful?
(check-lock))
(inner (void) after-load-file sucessful?))
(super-new)))
(define ports<%>
(interface ()
delete/io
get-insertion-point
set-insertion-point
get-unread-start-point
set-unread-start-point
set-allow-edits
get-allow-edits
insert-between
insert-before
submit-to-port?
on-submit
send-eof-to-in-port
send-eof-to-box-in-port
reset-input-box
clear-output-ports
clear-input-port
clear-box-input-port
get-out-style-delta
get-err-style-delta
get-value-style-delta
get-in-port
get-in-box-port
get-out-port
get-err-port
get-value-port
after-io-insertion
get-box-input-editor-snip%
get-box-input-text%))
(define-struct peeker (bytes skip-count pe resp-chan nack polling?) #:inspector (make-inspector))
(define-struct committer (kr commit-peeker-evt done-evt resp-chan resp-nack))
(define msec-timeout 500)
(define output-buffer-full 4096)
(define-local-member-name
new-box-input
box-input-not-used-anymore
set-port-text)
(define (set-box/f! b v) (when (box? b) (set-box! b v)))
(define arrow-cursor (make-object cursor% 'arrow))
(define eof-snip%
(class image-snip%
(init-field port-text)
(define/override (get-extent dc x y w h descent space lspace rspace)
(super get-extent dc x y w h descent space lspace rspace)
(set-box/f! descent 7)) ;; depends on actual bitmap used ...
(define/override (on-event dc x y editorx editory event)
(when (send event button-up? 'left)
(send port-text send-eof-to-box-in-port)))
(define/override (adjust-cursor dc x y edx edy e)
arrow-cursor)
(super-make-object (icon:get-eof-bitmap))
(inherit set-flags get-flags)
(set-flags (list* 'handles-events (get-flags)))))
(define out-style-name "text:ports out")
(define error-style-name "text:ports err")
(define value-style-name "text:ports value")
(let ([create-style-name
(λ (name sd)
(let* ([sl (editor:get-standard-style-list)])
(send sl new-named-style
name
(send sl find-or-create-style
(send sl find-named-style "Standard")
sd))))])
(let ([out-sd (make-object style-delta% 'change-nothing)])
(send out-sd set-delta-foreground (make-object color% 150 0 150))
(create-style-name out-style-name out-sd))
(let ([err-sd (make-object style-delta% 'change-italic)])
(send err-sd set-delta-foreground (make-object color% 255 0 0))
(create-style-name error-style-name err-sd))
(let ([value-sd (make-object style-delta% 'change-nothing)])
(send value-sd set-delta-foreground (make-object color% 0 0 175))
(create-style-name value-style-name value-sd)))
(define ports-mixin
(mixin (wide-snip<%>) (ports<%>)
(inherit begin-edit-sequence
change-style
delete
end-edit-sequence
find-snip
insert
get-canvas
get-start-position
get-end-position
get-snip-position
get-style-list
is-locked?
last-position
lock
paragraph-start-position
position-paragraph
release-snip
set-caret-owner
split-snip
get-focus-snip
get-view-size
scroll-to-position
position-location)
;; private field
(define eventspace (current-eventspace))
;; insertion-point : number
;; the place where the output ports insert data
;; only updated in `eventspace' (above)'s main thread
(define insertion-point 0)
;; unread-start-points : number
;; from this position to the end of the buffer is the
;; users editing that has not been committed to the
;; port.
;; only updated in `eventspace' (above)'s main thread
(define unread-start-point 0)
;; box-input : (union #f (is-a?/c editor-snip%))
;; the snip where the user's input is typed for the box input port
(define box-input #f)
(define eof-button (new eof-snip% (port-text this)))
;; allow-edits? : boolean
;; when this flag is set, only insert/delete after the
;; insertion-point are allowed.
(define allow-edits? #f)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; public interface
;;
;; insert-between : string/snp -> void
;; inserts something between the insertion point and the unread region
(define/public-final (insert-between str/snp)
(insert str/snp unread-start-point unread-start-point)
(set! unread-start-point (+ unread-start-point
(amt-of-space str/snp))))
;; insert-before : string/snp -> void
;; inserts something before both the insertion point and the unread region
(define/public-final (insert-before str/snp)
(insert str/snp insertion-point insertion-point)
(let ([amt (amt-of-space str/snp)])
(set! insertion-point (+ insertion-point amt))
(set! unread-start-point (+ unread-start-point amt))))
(define/private (amt-of-space str/snp)
(cond
[(string? str/snp) (string-length str/snp)]
[(is-a? str/snp snip%)
(send str/snp get-count)]))
(define/public-final (get-insertion-point) insertion-point)
(define/public-final (set-insertion-point ip) (set! insertion-point ip))
(define/public-final (get-unread-start-point)
unread-start-point)
(define/public-final (set-unread-start-point u)
(unless (<= u (last-position))
(error 'set-unread-start-point "~e is too large, last-position is ~e"
unread-start-point
(last-position)))
(set! unread-start-point u))
(define/public-final (set-allow-edits allow?) (set! allow-edits? allow?))
(define/public-final (get-allow-edits) allow-edits?)
(define/public-final (send-eof-to-in-port)
(channel-put read-chan (cons eof (position->line-col-pos unread-start-point))))
(define/public-final (send-eof-to-box-in-port)
(channel-put box-read-chan (cons eof (position->line-col-pos unread-start-point))))
(define/public-final (clear-input-port) (channel-put clear-input-chan (void)))
(define/public-final (clear-box-input-port) (channel-put box-clear-input-chan (void)))
(define/public-final (clear-output-ports)
(channel-put clear-output-chan (void))
(init-output-ports))
;; delete/io: number number -> void
(define/public-final (delete/io start end)
(unless (<= start end insertion-point)
(error 'delete/io "expected start (~a) <= end (~a) <= insertion-point (~a)"
start end insertion-point))
(let ([dist (- end start)])
(set! insertion-point (- insertion-point dist))
(set! unread-start-point (- unread-start-point dist)))
(let ([before-allowed? allow-edits?])
(set! allow-edits? #t)
(delete start end #f)
(set! allow-edits? before-allowed?)))
(define/public-final (get-in-port)
(unless in-port (error 'get-in-port "not ready"))
in-port)
(define/public-final (get-in-box-port)
(unless in-port (error 'get-in-box-port "not ready"))
in-box-port)
(define/public-final (get-out-port)
(unless out-port (error 'get-out-port "not ready"))
out-port)
(define/public-final (get-err-port)
(unless err-port (error 'get-err-port "not ready"))
err-port)
(define/public-final (get-value-port)
(unless value-port (error 'get-value-port "not ready"))
value-port)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; specialization interface
;;
(define/pubment (submit-to-port? key) (inner #t submit-to-port? key))
(define/pubment (on-submit) (inner (void) on-submit))
(define/public (get-out-style-delta) out-style-name)
(define/public (get-err-style-delta) error-style-name)
(define/public (get-value-style-delta) value-style-name)
(define/public (get-box-input-editor-snip%) editor-snip%)
(define/public (get-box-input-text%) input-box%)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; editor integration
;;
(define/augment (can-insert? start len)
(and (or allow-edits?
(start . >= . unread-start-point))
(inner #t can-insert? start len)))
(define/augment (can-delete? start len)
(and (or allow-edits?
(start . >= . unread-start-point))
(inner #t can-delete? start len)))
(define/override (on-local-char key)
(let ([start (get-start-position)]
[end (get-end-position)]
[code (send key get-key-code)])
(cond
[(not (or (eq? code 'numpad-enter)
(equal? code #\return)
(equal? code #\newline)))
(super on-local-char key)]
[(and (insertion-point . <= . start)
(= start end)
(submit-to-port? key))
(insert "\n")
(for-each/snips-chars
unread-start-point
(last-position)
(λ (s/c line-col-pos)
(cond
[(is-a? s/c snip%)
(channel-put read-chan (cons s/c line-col-pos))]
[(char? s/c)
(for-each (λ (b) (channel-put read-chan (cons b line-col-pos)))
(bytes->list (string->bytes/utf-8 (string s/c))))])))
(set! unread-start-point (last-position))
(set! insertion-point (last-position))
(on-submit)]
[else
(super on-local-char key)])))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; box input port management
;;
(define/public-final (reset-input-box)
(when box-input
(let ([l? (is-locked?)]
[old-allow-edits? allow-edits?])
(lock #f)
(set! allow-edits? #t)
(send box-input release-from-owner)
(send eof-button release-from-owner)
(set! unread-start-point (- unread-start-point 2))
(set! allow-edits? old-allow-edits?)
(lock l?))
(set! box-input #f)))
(define/private (adjust-box-input-width)
(when box-input
(let ([w (box 0)]
[x (box 0)]
[bw (send (icon:get-eof-bitmap) get-width)])
(get-view-size w #f)
(let ([pos (- (last-position) 2)])
(position-location pos x #f #t
(not (= pos (paragraph-start-position (position-paragraph pos))))))
(let ([size (- (unbox w) (unbox x) bw 24)])
(when (positive? size)
(send box-input set-min-width size))))))
(define/augment (on-display-size)
(adjust-box-input-width)
(inner (void) on-display-size))
(define/private (on-box-peek)
(unless box-input
(let* ([ed (new (get-box-input-text%))]
[es (new (get-box-input-editor-snip%)
(editor ed))]
[locked? (is-locked?)])
(begin-edit-sequence)
(send ed set-port-text this)
(lock #f)
#;(unless (= unread-start-point (paragraph-start-position (position-paragraph unread-start-point)))
(insert-between "\n"))
(insert-between es)
(insert-between eof-button)
#;(send (get-canvas) add-wide-snip es)
(set! box-input es)
(adjust-box-input-width)
(set-caret-owner es 'display)
(lock locked?)
(end-edit-sequence))))
(define/public (new-box-input ed)
(when (eq? ed (send box-input get-editor)) ;; just in case things get out of sync.
(let ([locked? (is-locked?)])
(begin-edit-sequence)
(send box-input set-min-width 'none)
(lock #f)
(let ([old-insertion-point insertion-point])
(let loop ([snip (send (send box-input get-editor) find-first-snip)])
(when snip
(let ([next (send snip next)])
(send snip release-from-owner)
(do-insertion
(list (cons (cond
[(is-a? snip string-snip%)
(send snip get-text 0 (send snip get-count))]
[else snip])
(make-object style-delta%)))
#t)
(loop next))))
;; this is copied code ...
(for-each/snips-chars
old-insertion-point
insertion-point
(λ (s/c line-col-pos)
(cond
[(is-a? s/c snip%)
(channel-put box-read-chan (cons s/c line-col-pos))]
[(char? s/c)
(for-each (λ (b) (channel-put box-read-chan (cons b line-col-pos)))
(bytes->list (string->bytes/utf-8 (string s/c))))]))))
(lock locked?)
(adjust-box-input-width)
(end-edit-sequence))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; output port syncronization code
;;
;; flush-chan : (channel (evt void))
;; signals that the buffer-thread should flush pending output
;; the evt inside is waited on to indicate the flush has occurred
(define flush-chan (make-channel))
;; clear-output-chan : (channel void)
(define clear-output-chan (make-channel))
;; write-chan : (channel (cons (union snip bytes) style))
;; send output to the editor
(define write-chan (make-channel))
;; readers-chan : (channel (list (channel (union byte snip))
;; (channel ...)))
(define readers-chan (make-channel))
;; queue-insertion : (listof (cons (union string snip) style)) evt -> void
;; txt is in the reverse order of the things to be inserted.
;; the evt is waited on when the text has actually been inserted
;; thread: any thread, except the eventspace main thread
(define/private (queue-insertion txts signal)
(parameterize ([current-eventspace eventspace])
(queue-callback
(λ ()
(do-insertion txts #f)
(sync signal)))))
;; do-insertion : (listof (cons (union string snip) style-delta)) boolean -> void
;; thread: eventspace main thread
(define/private (do-insertion txts showing-input?)
(let ([locked? (is-locked?)])
(begin-edit-sequence)
(lock #f)
(set! allow-edits? #t)
(let loop ([txts txts])
(cond
[(null? txts) (void)]
[else
(let* ([fst (car txts)]
[str/snp (car fst)]
[style (cdr fst)])
(let ([inserted-count
(if (is-a? str/snp snip%)
(send str/snp get-count)
(string-length str/snp))]
[old-insertion-point insertion-point])
(set! insertion-point (+ insertion-point inserted-count))
(set! unread-start-point (+ unread-start-point inserted-count))
(insert (if (is-a? str/snp snip%)
(send str/snp copy)
str/snp)
old-insertion-point
old-insertion-point
#t)
;; the idea here is that if you made a string snip, you
;; could have made a string and gotten the style, so you
;; must intend to have your own style.
(unless (is-a? str/snp string-snip%)
(change-style style old-insertion-point insertion-point))))
(loop (cdr txts))]))
(set! allow-edits? #f)
(lock locked?)
(unless showing-input?
(when box-input
(adjust-box-input-width)
(when (eq? box-input (get-focus-snip))
(scroll-to-position (last-position)))))
(end-edit-sequence)
(unless (null? txts)
(after-io-insertion))))
(define/public (after-io-insertion) (void))
(define output-buffer-thread
(let ([converter (bytes-open-converter "UTF-8-permissive" "UTF-8")])
(thread
(λ ()
(let loop (;; text-to-insert : (queue (cons (union snip bytes) style))
[text-to-insert (empty-queue)]
[last-flush (current-inexact-milliseconds)])
(sync
(if (queue-empty? text-to-insert)
never-evt
(handle-evt
(alarm-evt (+ last-flush msec-timeout))
(λ (_)
(let-values ([(viable-bytes remaining-queue) (split-queue converter text-to-insert)])
(queue-insertion viable-bytes always-evt)
(loop remaining-queue (current-inexact-milliseconds))))))
(handle-evt
flush-chan
(λ (return-evt)
(let-values ([(viable-bytes remaining-queue) (split-queue converter text-to-insert)])
(queue-insertion viable-bytes return-evt)
(loop remaining-queue (current-inexact-milliseconds)))))
(handle-evt
clear-output-chan
(λ (_)
(loop (empty-queue) (current-inexact-milliseconds))))
(handle-evt
write-chan
(λ (pr)
(let ([new-text-to-insert (enqueue pr text-to-insert)])
(cond
[((queue-size text-to-insert) . < . output-buffer-full)
(loop new-text-to-insert last-flush)]
[else
(let ([chan (make-channel)])
(let-values ([(viable-bytes remaining-queue)
(split-queue converter new-text-to-insert)])
(queue-insertion viable-bytes (channel-put-evt chan (void)))
(channel-get chan)
(loop remaining-queue (current-inexact-milliseconds))))]))))))))))
(field [in-port-args #f]
[out-port #f]
[err-port #f]
[value-port #f])
(define/private (init-output-ports)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; the following must be able to run
;; in any thread (even concurrently)
;;
(define (make-write-bytes-proc style)
(λ (to-write start end block/buffer? enable-breaks?)
(cond
[(= start end) (flush-proc)]
[(eq? (current-thread) (eventspace-handler-thread eventspace))
(error 'write-bytes-proc "cannot write to port on eventspace main thread")]
[else
(channel-put write-chan (cons (subbytes to-write start end) style))])
(- end start)))
(define (flush-proc)
(cond
[(eq? (current-thread) (eventspace-handler-thread eventspace))
(error 'flush-proc "cannot flush port on eventspace main thread")]
[else
(sync
(nack-guard-evt
(λ (fail-channel)
(let* ([return-channel (make-channel)]
[return-evt
(choice-evt
fail-channel
(channel-put-evt return-channel (void)))])
(channel-put flush-chan return-evt)
return-channel))))]))
(define (out-close-proc)
(void))
(define (make-write-special-proc style)
(λ (special can-buffer? enable-breaks?)
(cond
[(eq? (current-thread) (eventspace-handler-thread eventspace))
(error 'write-bytes-proc "cannot write to port on eventspace main thread")]
[else
(let ([str/snp (cond
[(string? special) special]
[(is-a? special snip%) special]
[else (format "~s" special)])])
(channel-put
write-chan
(cons str/snp style)))])
#t))
(let* ([add-standard
(λ (sd)
(cond
[(string? sd)
(let ([style-list (get-style-list)])
(or (send style-list find-named-style sd)
(send style-list find-named-style "Standard")
(send style-list find-named-style "Basic")))]
[sd
(let* ([style-list (get-style-list)]
[std (send style-list find-named-style "Standard")])
(if std
(send style-list find-or-create-style std sd)
(let ([basic (send style-list find-named-style "Basic")])
(send style-list find-or-create-style basic sd))))]))]
[out-style (add-standard (get-out-style-delta))]
[err-style (add-standard (get-err-style-delta))]
[value-style (add-standard (get-value-style-delta))])
(set! out-port (make-output-port #f
always-evt
(make-write-bytes-proc out-style)
out-close-proc
(make-write-special-proc out-style)))
(set! err-port (make-output-port #f
always-evt
(make-write-bytes-proc err-style)
out-close-proc
(make-write-special-proc err-style)))
(set! value-port (make-output-port #f
always-evt
(make-write-bytes-proc value-style)
out-close-proc
(make-write-special-proc value-style)))
(let ([install-handlers
(λ (port)
;; don't want to set the port-print-handler here;
;; instead drscheme sets the global-port-print-handler
;; to catch fractions and the like
(set-interactive-write-handler port)
(set-interactive-display-handler port))])
(install-handlers out-port)
(install-handlers err-port)
(install-handlers value-port))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; helpers
;;
;; type line-col-pos = (list (union #f fixnum) (union #f fixnum) (union #f fixnum)))
;; position->line-col-pos : number -> (list number number number)
(define/private (position->line-col-pos pos)
(let* ([para (position-paragraph pos)]
[para-start (paragraph-start-position para)])
(list (+ para 1)
(- pos para-start)
(+ pos 1))))
;; for-each/snips-chars : number number ((union char snip) line-col-pos -> void) -> void
(define/private (for-each/snips-chars start end func)
(split-snip start)
(split-snip end)
(let loop ([snip (find-snip start 'after-or-none)])
(cond
[(not snip) (void)]
[(< (get-snip-position snip) end)
(let ([line-col-pos (position->line-col-pos (get-snip-position snip))])
(cond
[(is-a? snip string-snip%)
(let ([str (send snip get-text 0 (send snip get-count))])
(let loop ([i 0])
(when (< i (string-length str))
(func (string-ref str i)
(list (car line-col-pos)
(+ i (cadr line-col-pos))
(+ i (caddr line-col-pos))))
(loop (+ i 1)))))
(loop (send snip next))]
[else
(func (send snip copy) line-col-pos)
(loop (send snip next))]))]
[else (void)])))
;; split-queue : converter (queue (cons (union snip bytes) style)
;; -> (values (listof (queue (cons (union snip bytes) style)) queue)
;; this function must only be called on the output-buffer-thread
;; extracts the viable bytes (and other stuff) from the front of the queue
;; and returns them as strings (and other stuff).
(define/private (split-queue converter q)
(let ([lst (queue->list q)])
(let loop ([lst lst]
[acc null])
(if (null? lst)
(values (reverse acc)
(empty-queue))
(let-values ([(front rest) (peel lst)])
(cond
[(not front) (values (reverse acc)
(empty-queue))]
[(bytes? (car front))
(let ([the-bytes (car front)]
[key (cdr front)])
(if (null? rest)
(let-values ([(converted-bytes src-read-k termination)
(bytes-convert converter the-bytes)])
(if (eq? termination 'aborts)
(values (reverse (cons (cons (bytes->string/utf-8 converted-bytes) key) acc))
(enqueue
(cons (subbytes the-bytes
src-read-k
(bytes-length the-bytes))
key)
(empty-queue)))
(values (reverse (cons (cons (bytes->string/utf-8 converted-bytes) key) acc))
(empty-queue))))
(let-values ([(converted-bytes src-read-k termination)
(bytes-convert converter the-bytes)]
[(more-bytes more-termination) (bytes-convert-end converter)])
(loop rest
(cons (cons (bytes->string/utf-8 (bytes-append converted-bytes more-bytes))
key)
acc)))))]
[else (loop rest
(cons front acc))]))))))
;; peel : (cons (cons (union snip bytes) X) (listof (cons (union snip bytes) X))
;; -> (values (cons (union snip bytes) X) (listof (cons (union snip bytes) X)
;; finds the first segment of bytes with the same style and combines them,
;; otherwise a lot like (define (peel x) (values (car x) (cdr x)))
(define/private (peel lst)
(let loop ([lst lst]
[acc #f]
[key #f])
(cond
[(null? lst) (values (cons acc key) null)]
[else
(let* ([fst (car lst)]
[fst-key (cdr fst)]
[fst-val (car fst)])
(cond
[(and (not key) (bytes? fst-val))
(loop (cdr lst)
fst-val
fst-key)]
[(and key (bytes? fst-val) (eq? key fst-key))
(loop (cdr lst)
(bytes-append acc fst-val)
key)]
[(not key)
(values fst (cdr lst))]
[else (if acc
(values (cons acc key) lst)
(values fst (cdr lst)))]))])))
(super-new)
(init-output-ports)
(define-values (in-port read-chan clear-input-chan)
(start-text-input-port this #f))
(define-values (in-box-port box-read-chan box-clear-input-chan)
(start-text-input-port this (lambda () (on-box-peek))))))
(define input-box<%>
(interface ((class->interface text%))
))
(define input-box-mixin
(mixin ((class->interface text%)) (input-box<%>)
(inherit erase lock)
(define port-text #f)
(define/public (set-port-text pt) (set! port-text pt))
(define in-use? #t)
(define/public (box-input-not-used-anymore)
(lock #t)
(set! in-use? #f))
(define/override (on-default-char kevt)
(super on-default-char kevt)
(when in-use?
(case (send kevt get-key-code)
[(numpad-enter #\return)
(send port-text new-box-input this)]
[else (void)])))
(super-new)))
(define (start-text-input-port source on-peek)
;; eventspace at the time this function was called. used for peek callbacks
(define eventspace (current-eventspace))
;; read-chan : (channel (cons (union byte snip eof) line-col-pos))
;; send input from the editor
(define read-chan (make-channel))
;; clear-input-chan : (channel void)
(define clear-input-chan (make-channel))
;; progress-event-chan : (channel (cons (channel event) nack-evt)))
(define progress-event-chan (make-channel))
;; peek-chan : (channel peeker)
(define peek-chan (make-channel))
;; commit-chan : (channel committer)
(define commit-chan (make-channel))
;; position-chan : (channel (cons (channel void) (channel line-col-pos)))
(define position-chan (make-channel))
(define input-buffer-thread
(thread
(λ ()
;; these vars are like arguments to the loop function
;; they are only set right before loop is called.
;; This is done to avoid passing the same arguments
;; over and over to loop.
(define peeker-sema (make-semaphore 0))
(define peeker-evt (semaphore-peek-evt peeker-sema))
(define bytes-peeked 0)
(define response-evts '())
(define peekers '()) ;; waiting for a peek
(define committers '()) ;; waiting for a commit
(define positioners '()) ;; waiting for a position
(define data (empty-queue)) ;; (queue (cons (union byte snip eof) line-col-pos))
(define position #f)
;; loop : -> alpha
;; the main loop for this thread
(define (loop)
(let-values ([(not-ready-peekers new-peek-response-evts)
(separate peekers service-waiter)]
[(potential-commits new-commit-response-evts)
(separate
committers
(service-committer data peeker-evt))])
(when (and on-peek
(not (null? not-ready-peekers)))
(parameterize ([current-eventspace eventspace])
(queue-callback on-peek)))
(set! peekers not-ready-peekers)
(set! committers potential-commits)
(set! response-evts
(append response-evts
new-peek-response-evts
new-commit-response-evts))
(sync
(handle-evt
position-chan
(λ (pr)
(let ([nack-chan (car pr)]
[resp-chan (cdr pr)])
(set! positioners (cons pr positioners))
(loop))))
(apply choice-evt (map service-positioner positioners))
(handle-evt
read-chan
(λ (ent)
(set! data (enqueue ent data))
(unless position
(set! position (cdr ent)))
(loop)))
(handle-evt
clear-input-chan
(λ (_)
(semaphore-post peeker-sema)
(set! peeker-sema (make-semaphore 0))
(set! peeker-evt (semaphore-peek-evt peeker-sema))
(set! data (empty-queue))
(set! position #f)
(loop)))
(handle-evt
progress-event-chan
(λ (return-pr)
(let ([return-chan (car return-pr)]
[return-nack (cdr return-pr)])
(set! response-evts
(cons (choice-evt
return-nack
(channel-put-evt return-chan peeker-evt))
response-evts))
(loop))))
(handle-evt
peek-chan
(λ (peeker)
(set! peekers (cons peeker peekers))
(loop)))
(handle-evt
commit-chan
(λ (committer)
(set! committers (cons committer committers))
(loop)))
(apply
choice-evt
(map
(λ (a-committer)
(match a-committer
[($ committer
kr
commit-peeker-evt
done-evt
resp-chan
resp-nack)
(choice-evt
(handle-evt
commit-peeker-evt
(λ (_)
;; this committer will be thrown out in next iteration
(loop)))
(handle-evt
done-evt
(λ (v)
(let ([nth-pos (cdr (peek-n data (- kr 1)))])
(set! position
(list (car nth-pos)
(+ 1 (cadr nth-pos))
(+ 1 (caddr nth-pos)))))
(set! data (dequeue-n data kr))
(semaphore-post peeker-sema)
(set! peeker-sema (make-semaphore 0))
(set! peeker-evt (semaphore-peek-evt peeker-sema))
(set! committers (remq a-committer committers))
(set! response-evts
(cons
(choice-evt
resp-nack
(channel-put-evt resp-chan #t))
response-evts))
(loop))))]))
committers))
(apply choice-evt
(map (λ (resp-evt)
(handle-evt
resp-evt
(λ (_)
(set! response-evts (remq resp-evt response-evts))
(loop))))
response-evts)))))
;; service-positioner : (cons (channel void) (channel line-col-pos)) -> evt
(define (service-positioner pr)
(let ([nack-evt (car pr)]
[resp-evt (cdr pr)])
(handle-evt
(choice-evt nack-evt
(channel-put-evt resp-evt (or position
;; a bogus position for when
;; nothing has happened yet.
(list 1 0 1))))
(let ([sent-position position])
(λ (_)
(set! positioners (remq pr positioners))
(loop))))))
;; service-committer : queue evt -> committer -> (union #f evt)
;; if the committer can be dumped, return an evt that
;; does the dumping. otherwise, return #f
(define ((service-committer data peeker-evt) a-committer)
(match a-committer
[($ committer
kr commit-peeker-evt
done-evt resp-chan resp-nack)
(let ([size (queue-size data)])
(cond
[(not (eq? peeker-evt commit-peeker-evt))
(choice-evt
resp-nack
(channel-put-evt resp-chan #f))]
[(< size kr)
(choice-evt
resp-nack
(channel-put-evt resp-chan 'commit-failure))]
[else ;; commit succeeds
#f]))]))
;; service-waiter : peeker -> (union #f evt)
;; if the peeker can be serviced, build an event to service it
;; otherwise return #f
(define (service-waiter a-peeker)
(match a-peeker
[($ peeker bytes skip-count pe resp-chan nack-evt polling?)
(cond
[(and pe (not (eq? pe peeker-evt)))
(choice-evt (channel-put-evt resp-chan #f)
nack-evt)]
[((queue-size data) . > . skip-count)
(let ([nth (car (peek-n data skip-count))])
(choice-evt
nack-evt
(cond
[(byte? nth)
(bytes-set! bytes 0 nth)
(channel-put-evt resp-chan 1)]
[(eof-object? nth)
(channel-put-evt resp-chan nth)]
[else
(channel-put-evt
resp-chan
(λ (src line col pos)
(if (is-a? nth readable-snip<%>)
(send nth read-special src line col pos)
nth)))])))]
[polling?
(choice-evt
nack-evt
(channel-put-evt resp-chan 0))]
[else
#f])]))
;; separate (listof X) (X -> (union #f Y)) -> (values (listof X) (listof Y))
;; separates `eles' into two lists -- those that `f' returns #f for
;; and then the results of calling `f' for those where `f' doesn't return #f
(define (separate eles f)
(let loop ([eles eles]
[transformed '()]
[left-alone '()])
(cond
[(null? eles) (values left-alone transformed)]
[else (let* ([ele (car eles)]
[maybe (f ele)])
(if maybe
(loop (cdr eles)
(cons maybe transformed)
left-alone)
(loop (cdr eles)
transformed
(cons ele left-alone))))])))
;;; start things going
(loop))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; the following must be able to run
;; in any thread (even concurrently)
;;
(define (read-bytes-proc bstr)
(let* ([progress-evt (progress-evt-proc)]
[v (peek-proc bstr 0 progress-evt)])
(cond
[(sync/timeout 0 progress-evt)
0]
[else
(wrap-evt
v
(λ (v)
(if (and (number? v) (zero? v))
0
(if (commit-proc (if (number? v) v 1)
progress-evt
always-evt)
v
0))))])))
(define (peek-proc bstr skip-count progress-evt)
(poll-guard-evt
(lambda (polling?)
(let ([evt
(nack-guard-evt
(λ (nack)
(let ([chan (make-channel)])
(channel-put peek-chan (make-peeker bstr skip-count progress-evt chan nack polling?))
chan)))])
(if polling?
(let ([v (sync evt)])
(if (eq? v 0)
;; Don't return 0, because that means something is
;; probably ready. We want to indicate that nothing is
;; ready.
never-evt
;; Even on success, package it as an event, because
;; `read-bytes-proc' expects an event
(wrap-evt always-evt (lambda (_) v))))
evt)))))
(define (progress-evt-proc)
(sync
(nack-guard-evt
(λ (nack)
(let ([chan (make-channel)])
(channel-put progress-event-chan (cons chan nack))
chan)))))
(define (commit-proc kr progress-evt done-evt)
(sync
(nack-guard-evt
(λ (nack)
(let ([chan (make-channel)])
(channel-put commit-chan (make-committer kr progress-evt done-evt chan nack))
chan)))))
(define (close-proc) (void))
(define (position-proc)
(let ([chan (make-channel)])
(apply
values
(sync
(nack-guard-evt
(λ (fail)
(channel-put position-chan (cons fail chan))
chan))))))
(let ([p (make-input-port source
read-bytes-proc
peek-proc
close-proc
progress-evt-proc
commit-proc
position-proc)])
(port-count-lines! p)
(values p read-chan clear-input-chan)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; queues
;;
(define-struct queue (front back count) #:mutable)
(define (empty-queue) (make-queue '() '() 0))
(define (enqueue e q) (make-queue
(cons e (queue-front q))
(queue-back q)
(+ (queue-count q) 1)))
(define (queue-first q)
(flip-around q)
(let ([back (queue-back q)])
(if (null? back)
(error 'queue-first "empty queue")
(car back))))
(define (queue-rest q)
(flip-around q)
(let ([back (queue-back q)])
(if (null? back)
(error 'queue-rest "empty queue")
(make-queue (queue-front q)
(cdr back)
(- (queue-count q) 1)))))
(define (flip-around q)
(when (null? (queue-back q))
(set-queue-back! q (reverse (queue-front q)))
(set-queue-front! q '())))
(define (queue-empty? q) (zero? (queue-count q)))
(define (queue-size q) (queue-count q))
;; queue->list : (queue x) -> (listof x)
;; returns the elements in the order that successive deq's would have
(define (queue->list q)
(let ([ans (append (queue-back q) (reverse (queue-front q)))])
(set-queue-back! q ans)
(set-queue-front! q '())
ans))
;; dequeue-n : queue number -> queue
(define (dequeue-n queue n)
(let loop ([q queue]
[n n])
(cond
[(zero? n) q]
[(queue-empty? q) (error 'dequeue-n "not enough!")]
[else (loop (queue-rest q) (- n 1))])))
;; peek-n : queue number -> queue
(define (peek-n queue init-n)
(let loop ([q queue]
[n init-n])
(cond
[(zero? n)
(when (queue-empty? q)
(error 'peek-n "not enough; asked for ~a but only ~a available"
init-n
(queue-size queue)))
(queue-first q)]
[else
(when (queue-empty? q)
(error 'dequeue-n "not enough!"))
(loop (queue-rest q) (- n 1))])))
;;
;; end queue abstraction
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
#|
=== AUTOCOMPLETE ===
This module defines autocomplete-mixin, a mixin for editors that adds an
unintrusive autocompletion menu when a keystroke is pressed.
By default, the system works by reading the prefix whenever the autcomplete
keystroke is pressed, and then constructing a list of possible completions
by searching through the contents of the autocomplete-word-list parameter for all words
that share that prefix; when the user types another character or deletes a
character autocomplete-word-list is consulted again. This seems to be fast enough for
all but very large completion lists. However, the code has been designed
to allow more efficient implementations if that becomes necessary ---
all autocomplete-word-list manipulation functions are isolated to the autocompletion-cursor<%>
interface, which implements two main methods, narrow and widen, to add or subtract
a character from the current prefix, respectively. A trie-based implementation,
for instance, could implement narrow and widen in constant-time at the cost of more
memory and more time to build the initial data structure.
===
autocomplete<%>
=new methods=
get-all-words : -> (listof string)
returns a list of all of the possible words that the completion should choose from
get-autocomplete-border-color : -> color string
returns the color for the border of the autocompletion menu
get-autocomplete-background-color : -> color string
returns the background color for the autocompletion menu
get-autocomplete-selected-color : -> color string
returns the selected color for the autocompletion menu
===
autocomplete-mixin: mixin (editor<%> -> editor<%>)
The autocomplete-text mixin produces a class that implements
editor<%> and provides the following extra public methods:
=overridden methods=
on-paint
overridden to draw the autocompletion menu as necessary.
on-char
overridden to intercept keypress events to control the completions
menu.
====
autocompletion-cursor<%>
An autocompletion-cursor<%> abstracts over a set of completions
for a particular prefix. Typically an autocompletion-cursor<%>
implementation will be created with a particular initial prefix;
from then on the autocomplete-text system will manipulate it
using the narrow and widen methods in response to user input.
The autocompletion-cursor<%> interface defines the following
methods:
get-completions : -> (listof string)
Produces a list of all possible completions.
get-length : -> int
Produces the number of possible completions.
empty? : -> boolean
Determines if there are any completions in the given cursor.
narrow : char -> autocompletion-cursor<%>
Yields a new cursor that represents the subset of
the completions held by this cursor that are also
completions of this cursor's prefix followed by the
given character.
widen : -> autocompletion-cursor<%> | #f
Yields a new cursor that represents the completions
of this cursor's prefix with the last character
removed.
===
autocompletion-cursor%
The implementation of autcompletion-cursor<%> used
by the default get-completions method.
===
scrolling-cursor : mixin (autocompletion-cursor<%> -> scrolling-cursor<%>)
scrolling-cursor is a mixin that takes classes that implement
autocompletion-cursor<%> to classes that implement scrolling-cursor<%>
(not provided).
===
configuration parameters
These configuration parameters customize autocompletion behavior.
autocomplete-append-after : string parameter
designates text to insert after a completion. Default: ""
autocomplete-limit : positive int parameter
designates the maximum number of completions to show at a time. Default: 15
completion-mode-key : character parameter
designates the character that triggers autocompletion
|#
(define autocomplete<%>
(interface ((class->interface text%))
auto-complete
get-autocomplete-border-color
get-autocomplete-background-color
get-autocomplete-selected-color
completion-mode-key-event?
get-all-words
get-word-at))
;; ============================================================
;; auto-complete-text (mixin) implementation
(define selected-color (make-object color% 204 153 255))
(define autocomplete-mixin
(mixin ((class->interface text%)) (autocomplete<%>)
(inherit invalidate-bitmap-cache get-dc get-start-position get-end-position
find-wordbreak get-text position-location insert dc-location-to-editor-location)
; get-autocomplete-border-color : -> string
; the color of text in the autocomplete menu
(define/public (get-autocomplete-border-color) "black")
; get-background-color : -> string
; background color in the autocomplete menu
(define/public (get-autocomplete-background-color) "lavender")
; get-autocomplete-selected-color : -> string
; selected option background color in the autocomplete menu
(define/public (get-autocomplete-selected-color) selected-color)
(define/public (completion-mode-key-event? key-event)
(cond
[(and (eq? (send key-event get-key-code) #\.)
(send key-event get-control-down))
(or (eq? (system-type) 'macosx)
(not (preferences:get 'framework:menu-bindings)))]
[else
#f]))
(define/public (get-all-words) (get-completions/manuals #f))
(define completions-box #f) ; completions-box% or #f if no completions box is active right now
(define word-start-pos #f) ; start pos of that word, or #f if no autocompletion
(define word-end-pos #f) ; end pos of that word, or #f if none
; string -> scrolling-cursor<%> given a prefix, returns the possible completions
; given a word, produces a cursor that describes
; all possible completions. The default implementation of autocompletion-cursor%
; returns all strings from the get-all-words method (below)
; that have the given string as a prefix; it performs a
; linear-search at every narrow/widen.
(define/private (get-completions word)
(new autocompletion-cursor%
[word word]
[all-words (get-all-words)]))
(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 completions-box (not before?))
(send completions-box draw dc dx dy)))
;; (-> void)
;; Check for possible completions of the current word and give the user a menu for them.
(define/public-final (auto-complete)
(when (equal? (get-start-position) (get-end-position))
(let* ([end-pos (get-end-position)]
[word (get-word-at end-pos)]
[completion-cursor (get-completions word)])
(let ([start-pos (- end-pos (string-length word))])
(set! word-start-pos start-pos)
(set! word-end-pos end-pos)
(show-options word start-pos end-pos completion-cursor)))))
;; Number -> String
;; The word that ends at the current positon of the editor
(define/public (get-word-at current-pos)
(let ([start-pos (box current-pos)])
(find-wordbreak start-pos #f 'caret)
(get-text (unbox start-pos) current-pos)))
;; String Number Number scrolling-cursor<%> -> void
;; Popup a menu of the given words at the location of the end-pos. Each menu item
;; should change the current word to the word in the list.
(define/private (show-options word start-pos end-pos cursor)
(let ([x (box 0)]
[y (box 0)])
(position-location start-pos x y #f)
(set! completions-box (new completion-box%
[completions (new scroll-manager% [cursor cursor])]
[menu-x (unbox x)]
[menu-y (+ (unbox y) 2)]
[editor this]))
(send completions-box redraw)))
;; on-char must handle inputs for two modes: normal text mode and in-the-middle-of-autocompleting mode
;; perhaps it would be better to handle this using the state machine pattern
(define/override (on-char key-event)
(cond
[completions-box
(let ([code (send key-event get-key-code)]
[full? (not (send completions-box empty?))])
(cond
[(and full? (memq code '(up wheel-up)))
(send completions-box prev-item)]
[(and full?
(or (memq code '(down wheel-down))
(completion-mode-key-event? key-event)))
(send completions-box next-item)]
[(and full? (eq? code 'prior)) (send completions-box scroll-display-up)]
[(and full? (eq? code 'next)) (send completions-box scroll-display-down)]
[(eq? code 'release)
(void)]
[(eq? code #\backspace)
(widen-possible-completions)
(super on-char key-event)]
[(eq? code #\return)
(when full?
(insert-currently-selected-string))
(destroy-completions-box)]
[(and (char? code) (char-graphic? code))
(super on-char key-event)
(constrict-possible-completions code)]
[else
(destroy-completions-box)
(super on-char key-event)]))]
[(completion-mode-key-event? key-event)
(auto-complete)]
[else
(super on-char key-event)]))
;; on-event controls what happens with the mouse
(define/override (on-event mouse-event)
(cond
[completions-box
(let*-values ([(x) (send mouse-event get-x)]
[(y) (send mouse-event get-y)]
[(mouse-x mouse-y) (dc-location-to-editor-location x y)])
(if (and (send completions-box point-inside-menu? mouse-x mouse-y)
(not (send completions-box empty?)))
(cond
[(send mouse-event moving?)
(send completions-box handle-mouse-movement mouse-x mouse-y)
(super on-event mouse-event)]
[(send mouse-event button-down?)
(insert-currently-selected-string)
(destroy-completions-box)]
[else
(super on-event mouse-event)])
(super on-event mouse-event)))]
[else (super on-event mouse-event)]))
(define/private (constrict-possible-completions char)
(set! word-end-pos (add1 word-end-pos))
(let-values ([(x0 y0 x1 y1) (send completions-box get-menu-coordinates)])
(send completions-box narrow char)
(let-values ([(_ __ x1p y1p) (send completions-box get-menu-coordinates)])
(invalidate-bitmap-cache x0 y0 (max x1 x1p) (max y1 y1p)))))
(define/private (widen-possible-completions)
(let-values ([(x0 y0 x1 y1) (send completions-box get-menu-coordinates)])
(let ([reasonable? (send completions-box widen)])
(cond
[reasonable?
(let-values ([(_ __ x1p y1p) (send completions-box get-menu-coordinates)])
(invalidate-bitmap-cache x0 y0 (max x1 x1p) (max y1 y1p)))]
[else
(set! completions-box #f)
(invalidate-bitmap-cache x0 y0 x1 y1)]))))
;; destroy-completions-box : -> void
;; eliminates the active completions box
(define/private (destroy-completions-box)
(let-values ([(x0 y0 x1 y1) (send completions-box get-menu-coordinates)])
(set! completions-box #f)
(invalidate-bitmap-cache x0 y0 x1 y1)))
;; insert-currently-selected-string : -> void
;; inserts the string that is currently being autoselected
(define/private (insert-currently-selected-string)
(let ([css (send completions-box get-current-selection)])
(insert (string-append css (autocomplete-append-after)) word-start-pos word-end-pos)))
(super-new)))
;; ============================================================
;; autocompletion-cursor<%> implementations
(define autocompletion-cursor<%>
(interface ()
get-completions ; -> (listof string)
get-length ; -> int
empty? ; -> boolean
narrow ; char -> autocompletion-cursor<%>
widen)) ; -> autocompletion-cursor<%> | #f
(define scrolling-cursor<%>
(interface (autocompletion-cursor<%>)
items-are-hidden?
get-visible-completions
get-visible-length
scroll-down
scroll-up))
(define autocompletion-cursor%
(class* object% (autocompletion-cursor<%>)
(init-field word all-words)
(define/private (starts-with prefix)
(let ([re (regexp (string-append "^" (regexp-quote prefix)))])
(λ (w) (regexp-match re w))))
(define all-completions (filter (starts-with word) all-words))
(define all-completions-length (length all-completions))
(define/public (narrow c)
(new autocompletion-cursor%
[word (string-append word (list->string (list c)))]
[all-words all-words]))
(define/public (widen)
(let ([strlen (string-length word)])
(cond
[(< strlen 2) #f]
[else
(new autocompletion-cursor%
[word (substring word 0 (- (string-length word) 1))]
[all-words all-words])])))
(define/public (get-completions) all-completions)
(define/public (get-length) all-completions-length)
(define/public (empty?) (eq? (get-length) 0))
(super-new)))
(define scroll-manager%
(class* object% ()
(init-field cursor)
(define all-completions #f)
(define all-completions-length #f)
(define visible-completions #f)
(define visible-completions-length #f)
(define hidden? #f)
(define/private (initialize-state!)
(cond
[(<= (send cursor get-length) (autocomplete-limit))
(set! hidden? #f)
(set! all-completions (send cursor get-completions))
(set! all-completions-length (send cursor get-length))
(set! visible-completions all-completions)
(set! visible-completions-length all-completions-length)]
[else
(set! hidden? #t)
(set! all-completions (send cursor get-completions))
(set! all-completions-length (send cursor get-length))
(set! visible-completions (srfi1:take (send cursor get-completions) (autocomplete-limit)))
(set! visible-completions-length (autocomplete-limit))]))
(define/public (get-completions) all-completions)
(define/public (get-length) all-completions-length)
(define/public (empty?) (send cursor empty?))
(define/public (get-visible-length) visible-completions-length)
(define/public (get-visible-completions) visible-completions)
(define/public (items-are-hidden?) hidden?)
(define/public (scroll-down)
(when hidden?
(set! all-completions (append (srfi1:drop all-completions (autocomplete-limit)) visible-completions))
(set! visible-completions (srfi1:take all-completions (autocomplete-limit)))))
(define/public (scroll-up)
(when hidden?
(let ([n (- all-completions-length (autocomplete-limit))])
(set! all-completions (append (srfi1:drop all-completions n) (srfi1:take all-completions n)))
(set! visible-completions (srfi1:take all-completions (autocomplete-limit))))))
(define/public (narrow char)
(let ([new-cursor (send cursor narrow char)])
(set! cursor new-cursor)
(initialize-state!)))
(define/public (widen)
(let ([new-cursor (send cursor widen)])
(cond
[new-cursor
(set! cursor new-cursor)
(initialize-state!)
#t]
[else #f])))
(initialize-state!)
(super-new)))
;; ============================================================
;; completion-box<%> implementation
(define menu-padding-x 4)
(define menu-padding-y 0)
(define completion-box<%>
(interface ()
draw ; dc<%> int int -> void
redraw ; -> void
get-menu-coordinates ; -> (values int int int int)
next-item ; -> void
prev-item ; -> void
scroll-display-up ; -> void
scroll-display-down ; -> void
get-current-selection ; -> string
narrow ; char -> boolean
widen ; -> boolean
empty?)) ; -> boolean
(define hidden-completions-text "⋮")
(define-struct geometry (menu-x
menu-y
menu-width
menu-height
mouse->menu-item-vector))
(define completion-box%
(class* object% (completion-box<%>)
(init-field completions ; scroll-manager% the possible completions (all of which have base-word as a prefix)
menu-x ; int the menu's top-left x coordinate
menu-y ; int the menu's top-left y coordinate
editor ; editor<%> the owner of this completion box
)
(define/public (empty?) (send completions empty?))
(define/private (compute-geometry)
(define vec #f)
(define (initialize-mouse-offset-map! coord-map)
(cond
[(null? coord-map) (void)] ; is this possible?
[else
(let* ([last-index (cadr (car coord-map))]
[v (make-vector (add1 last-index))])
(for-each
(λ (elt)
(let ([first (car elt)]
[last (cadr elt)]
[val (caddr elt)])
(let loop ([n first])
(when (<= n last)
(vector-set! v n val)
(loop (add1 n))))))
coord-map)
(set! vec v))]))
(define-values (editor-width editor-height)
(let* ([wb (box 0)]
[hb (box 0)]
[admin (send editor get-admin)])
(if admin
(begin
(send admin get-view #f #f wb hb)
(values (unbox wb)
(unbox hb)))
(values 10 10))))
(let* ([num-completions (send completions get-length)]
[shown-completions (send completions get-visible-completions)])
(define-values (w h)
(let ([dc (send editor get-dc)])
(cond
[(zero? num-completions)
(let-values ([(tw th _1 _2) (send dc get-text-extent (string-constant no-completions)
(get-mt-font dc))])
(values (+ menu-padding-x tw menu-padding-x)
(+ menu-padding-y th menu-padding-y)))]
[else
(let loop ([pc shown-completions]
[w 0]
[h 0]
[coord-map '()]
[n 0])
(cond
[(null? pc)
(let-values ([(hidden?) (send completions items-are-hidden?)]
[(tw th _1 _2) (send dc get-text-extent hidden-completions-text)])
(let ([w (if hidden? (max tw w) w)]
[h (if hidden? (+ th h) h)])
(initialize-mouse-offset-map! coord-map)
(let ([offset-h menu-padding-y]
[offset-w (* menu-padding-x 2)])
(values (+ offset-w w)
(+ offset-h h)))))]
[else
(let ([c (car pc)])
(let-values ([(tw th _1 _2) (send dc get-text-extent c)])
(loop (cdr pc)
(max tw w)
(+ th h)
(cons (list (inexact->exact h) (inexact->exact (+ h th)) n) coord-map)
(add1 n))))]))])))
(let ([final-x (cond
[(< (+ menu-x w) editor-width)
menu-x]
[(> editor-width w)
(- editor-width w)]
[else menu-x])]
[final-y menu-y])
(make-geometry final-x final-y w h vec))))
;; geometry records the menu's current width and height and a vector associating mouse location with
;; selected item
(define geometry (compute-geometry))
(define highlighted-menu-item 0) ; the currently-highlighted menu item
;; draw : dc<%> int int -> void
;; draws the menu to the given drawing context at offset dx, dy
(define/public (draw dc dx dy)
(let ([old-pen (send dc get-pen)]
[old-brush (send dc get-brush)])
(send dc set-pen (send editor get-autocomplete-border-color) 1 'solid)
(send dc set-brush (send editor get-autocomplete-background-color) 'solid)
(let-values ([(mx my tw th) (get-menu-coordinates)])
(send dc draw-rectangle (+ mx dx) (+ my dy) tw th)
(cond
[(send completions empty?)
(let ([font (send dc get-font)])
(send dc set-font (get-mt-font dc))
(send dc draw-text (string-constant no-completions) (+ mx dx menu-padding-x) (+ menu-padding-y my dy))
(send dc set-font font))]
[else
(let loop ([item-number 0] [y my] [pc (send completions get-visible-completions)])
(cond
[(null? pc)
(when (send completions items-are-hidden?)
(let-values ([(hw _1 _2 _3) (send dc get-text-extent hidden-completions-text)])
(send dc draw-text
hidden-completions-text
(+ mx dx (- (/ tw 2) (/ hw 2)))
(+ menu-padding-y y dy))))]
[else
(let ([c (car pc)])
(let-values ([(w h d a) (send dc get-text-extent c)])
(when (= item-number highlighted-menu-item)
(send dc set-pen "black" 1 'transparent)
(send dc set-brush (send editor get-autocomplete-selected-color) 'solid)
(send dc draw-rectangle (+ mx dx 1) (+ dy y menu-padding-y 1) (- tw 2) (- h 1)))
(send dc draw-text c (+ mx dx menu-padding-x) (+ menu-padding-y y dy))
(loop (add1 item-number) (+ y h) (cdr pc))))]))]))
(send dc set-pen old-pen)
(send dc set-brush old-brush)))
(define/private (get-mt-font dc)
(let ([font (send dc get-font)])
(send the-font-list find-or-create-font
(send font get-point-size)
(send font get-family)
'italic
(send font get-weight)
(send font get-underlined)
(send font get-smoothing))))
;; redraw : -> void
;; tells the parent to refresh enough of itself to redraw this menu
(define/public (redraw)
(let-values ([(x y w h) (get-menu-coordinates)])
(send editor invalidate-bitmap-cache x y w h)))
;; get-menu-coordinates : -> (values int int int int)
;; get the menu's x, y, w, h coordinates with respect to its parent
(define/public (get-menu-coordinates)
(values (geometry-menu-x geometry)
(geometry-menu-y geometry)
(geometry-menu-width geometry)
(geometry-menu-height geometry)))
;; next-item : -> void
;; tells the menu that the next item is selected
(define/public (next-item)
(cond
[(and (= highlighted-menu-item (sub1 (autocomplete-limit)))
(send completions items-are-hidden?))
(set! highlighted-menu-item 0)
(scroll-display-down)]
[else
(set! highlighted-menu-item (modulo (add1 highlighted-menu-item) (send completions get-visible-length)))
(redraw)]))
;; prev-item : -> void
;; tells the menu that the previous item is selected
(define/public (prev-item)
(cond
[(and (= highlighted-menu-item 0)
(send completions items-are-hidden?))
(set! highlighted-menu-item
(sub1 (send completions get-visible-length)))
(scroll-display-up)]
[else
(set! highlighted-menu-item (modulo (sub1 highlighted-menu-item) (send completions get-visible-length)))
(redraw)]))
;; scroll-display-down : -> void
;; shows the next page possible completions
(define/private (scroll-display do-it!)
(let*-values ([(old-x1 old-y1 old-w old-h) (get-menu-coordinates)]
[(_) (do-it!)]
[(_) (set! geometry (compute-geometry))]
[(new-x1 new-y1 new-w new-h) (get-menu-coordinates)])
(let ([old-x2 (+ old-x1 old-w)]
[old-y2 (+ old-y1 old-h)]
[new-x2 (+ new-x1 new-w)]
[new-y2 (+ new-y1 new-h)])
(let ([composite-x1 (min old-x1 new-x1)]
[composite-y1 (min old-y1 new-y1)]
[composite-x2 (max old-x2 new-x2)]
[composite-y2 (max old-y2 new-y2)])
(send editor invalidate-bitmap-cache
composite-x1
composite-y1
(- composite-x2 composite-x1)
(- composite-y2 composite-y1))))))
(define/public (scroll-display-down)
(scroll-display (λ () (send completions scroll-down))))
(define/public (scroll-display-up)
(scroll-display (λ () (send completions scroll-up))))
;; point-inside-menu? : nat nat -> boolean
;; determines if the given x,y editor coordinate is inside
;; the drawn window or not
(define/public (point-inside-menu? x y)
(let*-values ([(mx my w h) (get-menu-coordinates)])
(and (<= mx x (+ mx w))
(<= my y (+ my h)))))
;; handle-mouse-movement : int int -> bool
;; takes an editor coordinate, returns whether it has intercept
(define/public (handle-mouse-movement x y)
(let*-values ([(mx my w h) (get-menu-coordinates)])
(when (and (<= mx x (+ mx w))
(< (+ my menu-padding-y) y (+ my (vector-length (geometry-mouse->menu-item-vector geometry)))))
(set! highlighted-menu-item (vector-ref (geometry-mouse->menu-item-vector geometry) (inexact->exact (- y my))))
(redraw))))
;; get-current-selection : -> string
;; returns the selected string
(define/public (get-current-selection)
(list-ref (send completions get-visible-completions) highlighted-menu-item))
;; narrow : char -> boolean
;; narrows the given selection given a new character (faster than recomputing the whole thing)
(define/public (narrow char)
(send completions narrow char)
(set! highlighted-menu-item 0)
(set! geometry (compute-geometry))
(not (send completions empty?)))
;; widen : -> boolean
;; attempts widens the selection by eliminating the last character from the word.
;; returns #f if that cannot be done (because there are no characters left); #t otherwise
(define/public (widen)
(let ([successfully-widened? (send completions widen)])
(cond
[successfully-widened?
(set! highlighted-menu-item 0)
(set! geometry (compute-geometry))
#t]
[else #f])))
(super-new)))
;; ============================================================
;; configuration parameters
(define (make-guarded-parameter name description default okay?)
(make-parameter
default
(λ (v)
(cond
[(okay? v) v]
[else
(raise (make-exn:fail:contract
(string->immutable-string
(format "parameter ~a: expected ~a, given: ~e" name description v))))]))))
(define autocomplete-append-after
(make-guarded-parameter 'append-after "string" "" string?))
(define autocomplete-limit
(make-guarded-parameter 'limit "positive integer" 15 (λ (x) (and (integer? x) (> x 0)))))
;; ============================================================
;; read keywords from manuals
(define xref #f)
(define (get-completions/manuals manuals)
(let* ([sym->mpi (λ (mp) (module-path-index-resolve (module-path-index-join mp #f)))]
[manual-mpis (and manuals (map sym->mpi manuals))])
(unless xref
(set! xref (load-collections-xref)))
(let ([ht (make-hash)])
(for-each
(λ (entry)
(let ([desc (entry-desc entry)])
(when (exported-index-desc? desc)
(let ([name (exported-index-desc-name desc)])
(when name
(when (or (not manual-mpis)
(ormap (λ (from-lib) (memq from-lib manual-mpis))
(map sym->mpi (exported-index-desc-from-libs desc))))
(hash-set! ht (symbol->string name) #t)))))))
(xref-index xref))
(sort (hash-map ht (λ (x y) x)) string<=?))))
;; ============================================================
;; auto complete example code
#;
(begin
(define all-words (get-completions/manuals #f))
(let* ([f (new frame% (label "Test") (height 400) (width 400))]
[e (new (autocomplete-mixin text%))]
[c (new editor-canvas% (editor e) (parent f))])
(send c focus)
(send e insert "\n\n get")
(send e set-position (send e last-position) (send e last-position))
(send f show #t)))
(define basic% (basic-mixin (editor:basic-mixin text%)))
(define hide-caret/selection% (hide-caret/selection-mixin basic%))
(define nbsp->space% (nbsp->space-mixin basic%))
(define delegate% (delegate-mixin basic%))
(define wide-snip% (wide-snip-mixin basic%))
(define standard-style-list% (editor:standard-style-list-mixin wide-snip%))
(define input-box% (input-box-mixin standard-style-list%))
(define -keymap% (editor:keymap-mixin standard-style-list%))
(define return% (return-mixin -keymap%))
(define autowrap% (editor:autowrap-mixin -keymap%))
(define file% (file-mixin (editor:file-mixin autowrap%)))
(define clever-file-format% (clever-file-format-mixin file%))
(define backup-autosave% (editor:backup-autosave-mixin clever-file-format%))
(define searching% (searching-mixin backup-autosave%))
(define info% (info-mixin (editor:info-mixin searching%)))