;;; expeditor.ss
;;; R. Kent Dybvig
;;; August 2007

;;; This code is based on David Boyer's command-line editor, which has the
;;; following copyright:
;;;
;;;   Copyright (c) 1989, 1993, 1994 C. David Boyer
;;;
;;;   Permission to copy this software, in whole or in part, to use this
;;;   software for any lawful purpose, and to redistribute this software is
;;;   granted subject to the restriction that all copies made of this software
;;;   must include this copyright notice in full.
;;;
;;; The present implementation retains some of the basic design but little
;;; of the original code.

;;; The expression editor module is organized into sections:
;;;
;;;  1. screen-management routines
;;;  2. exported parameters
;;;  3. eestate and pos record definitions
;;;  4. current entry management routines
;;;  5. the reader and prompt-and-reader
;;;  6. history management routines
;;;  7. key function definitions
;;;  8. key binding code
;;;
;;; Also contained within this file are a few system entry points:
;;; the $enable-expeditor and $expeditor-history-file parameters and
;;; the main entry point into the expression editor, $expeditor.

(when-feature expeditor

(define $enable-expeditor (make-parameter #f))
(define $expeditor-history-file
  (make-parameter #f
    (lambda (s)
      (cond
        [(not s) s]
        [(string? s)
         (if (string=? s "")
             (if-feature windows
               (cond
                 [(getenv "APPDATA") =>
                  (lambda (appdata)
                    (let ([dir (format "~a\\Chez Scheme" appdata)])
                      (unless (file-exists? dir)
                        (guard (c [#t (void)]) (mkdir dir)))
                      (format "~a\\History" dir)))]
                 [(getenv "HOME") =>
                  (lambda (home)
                    (format "~a\\.chezscheme_history" home))]
                 [else ".chezscheme_history"])
               "~/.chezscheme_history")
             s)]
        [else ($oops '$expeditor-history-file "~s is not #f or a string" s)]))))
          
(define $expeditor)

(module expression-editor
            (
            ; parameters
             ee-auto-indent ee-auto-paren-balance ee-common-identifiers
             ee-default-repeat ee-flash-parens ee-noisy
             ee-paren-flash-delay ee-history-limit ee-standard-indent
            ; establishing key bindings
             ee-bind-key ee-compose
            ; built-in operators
             ee-next-id-completion
             ee-next-id-completion/indent
             ee-id-completion ee-id-completion/indent
             ee-insert-self ee-command-repeat
             ee-history-bwd ee-history-fwd
             ee-history-fwd-prefix ee-history-bwd-prefix
             ee-history-fwd-contains ee-history-bwd-contains
             ee-newline ee-accept ee-newline/accept ee-open-line
             ee-indent ee-indent-all ee-backward-char
             ee-forward-char ee-next-line ee-previous-line
             ee-end-of-line ee-beginning-of-line
             ee-beginning-of-entry ee-end-of-entry
             ee-delete-to-eol ee-delete-line
             ee-delete-between-point-and-mark ee-set-mark
             ee-delete-entry ee-reset-entry ee-delete-sexp ee-backward-delete-sexp
             ee-redisplay ee-yank-kill-buffer ee-yank-selection
             ee-string-macro ee-eof ee-delete-char ee-eof/delete-char
             ee-backward-delete-char ee-insert-paren
             ee-flash-matching-delimiter ee-goto-matching-delimiter
             ee-exchange-point-and-mark ee-forward-sexp
             ee-backward-sexp ee-forward-word
             ee-backward-word ee-forward-page
             ee-backward-page ee-suspend-process
            )

(define-syntax assert*
  (syntax-rules ()
    [(_ expr ...)
     (begin (assert expr) ...)]))

(define-syntax on-error
  (syntax-rules ()
    [(on-error e0 e1 e2 ...)
     (guard (c [#t e0]) e1 e2 ...)]))

(define-syntax defopt
  (syntax-rules ()
    [(_ (p x ... [y e]) b1 b2 ...)
     (define p
       (case-lambda
         [(x ...) (p x ... e)]
         [(x ... y) b1 b2 ...]))]))

; screen initialization and manipulation routines

(module (init-screen raw-mode no-raw-mode
         screen-resize! screen-rows screen-cols
         ee-winch? ee-char-ready? ee-peek-char ee-read-char
         ee-write-char ee-display-string ee-flush
         move-cursor-up move-cursor-right move-cursor-left move-cursor-down
         scroll-reverse clear-eol clear-eos clear-screen
         carriage-return line-feed
         bell pause get-clipboard wait)
 ; screen state
  (define cols)
  (define rows)
  (define cursor-col)
  (define the-unread-char)
  (define winch)

 ; we use terminfo routines directly, rather than going through curses,
 ; because curses requires initscr(), which clears the screen, discarding
 ; the current context.  this is a shell, not a full-screen user interface.

  (define init-term (foreign-procedure "(cs)ee_init_term" () boolean))
  (define $ee-read-char (foreign-procedure "(cs)ee_read_char" (boolean) scheme-object))
  (define $ee-write-char (foreign-procedure "(cs)ee_write_char" (wchar_t) void))
  (define ee-flush (foreign-procedure "(cs)ee_flush" () void))
  (define get-screen-size (foreign-procedure "(cs)ee_get_screen_size" () scheme-object))
  (define raw-mode (foreign-procedure "(cs)ee_raw" () void))
  (define no-raw-mode (foreign-procedure "(cs)ee_noraw" () void))
  (define enter-am-mode (foreign-procedure "(cs)ee_enter_am_mode" () void))
  (define exit-am-mode (foreign-procedure "(cs)ee_exit_am_mode" () void))
  (define nanosleep (foreign-procedure "(cs)ee_nanosleep" (unsigned-32 unsigned-32) void))
  (define pause (foreign-procedure "(cs)ee_pause" () void))
  (define get-clipboard (foreign-procedure "(cs)ee_get_clipboard" () scheme-object))

  (define move-cursor-up (foreign-procedure "(cs)ee_up" (integer-32) void))
  (define move-cursor-down (foreign-procedure "(cs)ee_down" (integer-32) void))
  (define $move-cursor-left (foreign-procedure "(cs)ee_left" (integer-32) void))
  (define $move-cursor-right (foreign-procedure "(cs)ee_right" (integer-32) void))
  (define clear-eol (foreign-procedure "(cs)ee_clr_eol" () void))
  (define clear-eos (foreign-procedure "(cs)ee_clr_eos" () void))
  (define $clear-screen (foreign-procedure "(cs)ee_clear_screen" () void))
  (define scroll-reverse (foreign-procedure "(cs)ee_scroll_reverse" (integer-32) void))
  (define bell (foreign-procedure "(cs)ee_bell" () void))
  (define $carriage-return (foreign-procedure "(cs)ee_carriage_return" () void))
  (define line-feed (foreign-procedure "(cs)ee_line_feed" () void))

  (define (screen-resize!)
    (let ([p (get-screen-size)])
      (set! rows (car p))
      (set! cols (cdr p))))

  (define (screen-rows) rows)
  (define (screen-cols) cols)

  (define (init-screen)
    (and (init-term)
         (begin
           (set! cursor-col 0)
           (set! the-unread-char #f)
           (set! winch #f)
           #t)))

  (define (clear-screen)
    ($clear-screen)
    (set! cursor-col 0))

  (define (ee-winch?)
    (and (not the-unread-char)
      (if winch
          (begin (set! winch #f) #t)
          (begin
            (ee-flush)
            (let ([c ($ee-read-char #t)])
              (or (eq? c #t)
                  (begin (set! the-unread-char c) #f)))))))

  (define (ee-char-ready?)
    (if the-unread-char
        #t
        (let f ()
          (ee-flush)
          (let ([c ($ee-read-char #f)])
            (cond
              [(eq? c #f) #f]
              [(eq? c #t) (set! winch #t) (f)]
              [else (set! the-unread-char c) #t])))))

  (define (ee-read-char)
    (if the-unread-char
        (let ([c the-unread-char]) (set! the-unread-char #f) c)
        (let f ()
          (ee-flush)
          (let ([c ($ee-read-char #t)])
            (if (eq? c #t)
                (begin (set! winch #t) (f))
                c)))))

  (define (ee-peek-char)
    (or the-unread-char
        (let ([c (ee-read-char)])
          (set! the-unread-char c)
          c)))

 ; we assume that ee-write-char receives only characters that occupy one
 ; screen cell.  it should never be passed #\return, #\newline, or #\tab.
 ; furthermore, ee-write-char should never be used to write past the end
 ; of a screen line.
  (define (ee-write-char c)
    (set! cursor-col (fx+ cursor-col 1))
    (if (fx= cursor-col cols)
        (begin
          (exit-am-mode)
          ($ee-write-char c)
          (enter-am-mode))
        ($ee-write-char c)))

 ; comments regarding ee-write-char above apply also to ee-display-string
  (define (ee-display-string s)
    (let ([n (string-length s)])
      (do ([i 0 (fx+ i 1)])
          ((fx= i n))
        (ee-write-char (string-ref s i)))))

  (define (carriage-return)
    (set! cursor-col 0)
    ($carriage-return))

  (define (move-cursor-right n)
    (cond
      [(fx< (fx+ cursor-col n) cols)
       ($move-cursor-right n)
       (set! cursor-col (fx+ cursor-col n))]
      [else
       (move-cursor-down (quotient (fx+ cursor-col n) cols))
       (let ([new-cursor-col (remainder (fx+ cursor-col n) cols)])
         (if (fx>= new-cursor-col cursor-col)
             (move-cursor-right (fx- new-cursor-col cursor-col))
             (move-cursor-left (fx- cursor-col new-cursor-col))))]))

  (define (move-cursor-left n)
    (when (and (fx= cursor-col cols) (fx> n 0))
      (set! n (fx- n 1))
      (set! cursor-col (fx- cursor-col 1)))
    (cond
      [(fx<= n cursor-col)
       ($move-cursor-left n)
       (set! cursor-col (fx- cursor-col n))]
      [else
       (move-cursor-up (fx1+ (quotient (fx- n cursor-col 1) cols)))
       (let ([new-cursor-col (remainder
                               (fx- cols (remainder (fx- n cursor-col) cols))
                               cols)])
         (if (fx>= new-cursor-col cursor-col)
             (move-cursor-right (fx- new-cursor-col cursor-col))
             (move-cursor-left (fx- cursor-col new-cursor-col))))]))

  (define wait
    (lambda (ms)
      (unless (or (<= ms 0) (ee-char-ready?))
        (nanosleep 0 (* 10 1000 1000)) ; 10ms granularity is best we can assume
        (wait (- ms 10)))))
)

;;; parameters

(define ee-common-identifiers
  (make-parameter
   ; general theory: exclude short ids and ids that will come up early
   ; in an alphabetical search with short prefix.  include common ids that
   ; come up annoyingly late in such a search.
    '(append apply call/cc call-with-values define display display-string
      define-syntax define-record null? quote quotient reverse read-char
      substring string-ref string-length string? string=? string-set!
      syntax-case syntax-rules unless vector-ref vector-length vector?
      vector-set! vector)
    (lambda (x)
      (unless (and (list? x) (andmap symbol? x))
        ($oops 'ee-common-identifiers "~s is not a list of symbols" x))
      x)))

;;; default repeat value for ^U
(define ee-default-repeat
  (make-parameter 4
    (lambda (x)
      (unless (and (fixnum? x) (fxnonnegative? x))
        ($oops 'ee-default-repeat "~s is not an integer" x))
      x)))

(define ee-auto-indent (make-parameter #t (lambda (x) (and x #t))))

(define ee-auto-paren-balance (make-parameter #t (lambda (x) (and x #t))))

(define ee-flash-parens (make-parameter #t (lambda (x) (and x #t))))

;;; paren balance delay factor in milliseconds
(define ee-paren-flash-delay
  (make-parameter 100
    (lambda (x)
      (unless (and (fixnum? x) (fxnonnegative? x))
        ($oops 'ee-paren-flash-delay "~s is not an integer" x))
      x)))

;;; enable/disable bell
(define ee-noisy (make-parameter #f (lambda (x) (and x #t))))

;;; standard indent length
(define ee-standard-indent
  (make-parameter 2
    (lambda (x)
      (unless (and (fixnum? x) (fxnonnegative? x))
        ($oops 'ee-standard-indent "~s is not an integer" x))
      x)))

(define ee-history-limit
  (make-parameter 256
    (lambda (x)
      (unless (and (fixnum? x) (fxnonnegative? x))
        ($oops 'ee-history-length "~s is not a nonnegative fixnum" x))
      x)))

;;; eestate holds the state of the expression editor.

(define-record-type eestate
  (fields
    (mutable last-op)
    (mutable rt-last-op)
    (mutable prompt)
    (mutable repeat-count)
    (mutable killbuf)
    (mutable histnew)
    (mutable histbwd)
    (mutable histnow)
    (mutable histfwd)
    (mutable histkey)
    (mutable last-suffix*)
    (mutable cc?))
  (nongenerative)
  (sealed #t)
  (protocol
    (lambda (new)
      (lambda ()
        (new #f         ; last-op
             '(0 . 0)   ; rt-last-op
             ""         ; prompt
             1          ; repeat-count
             '()        ; killbuf
             0          ; histnew
             '()        ; histbwd
             ""         ; histnow
             '()        ; hisfwd
             ""         ; histkey
             '()        ; last-suffix*
             #f)))))     ; cc?

(module ()
  (record-writer (type-descriptor eestate)
    (lambda (x p wr)
      (display "#<ee-state>" p))))

;;; pos is used for two different but related purposes: for row, col
;;; positions and for row, physical-line positions.  see the comment
;;; about the entry top-line and bot-line fields below.

(module (make-pos pos? pos-row pos-col pos=? pos<? pos<=? pos>? pos>=? index->pos)
  (define-record-type pos
    (fields (immutable row) (immutable col))
    (nongenerative)
    (sealed #t))
  (define (pos=? p1 p2)
    (and (fx= (pos-row p1) (pos-row p2))
         (fx= (pos-col p1) (pos-col p2))))
  (define (pos<? p1 p2)
    (or (fx< (pos-row p1) (pos-row p2))
        (and (fx= (pos-row p1) (pos-row p2))
             (fx< (pos-col p1) (pos-col p2)))))
  (define (pos<=? p1 p2)
    (or (fx< (pos-row p1) (pos-row p2))
        (and (fx= (pos-row p1) (pos-row p2))
             (fx<= (pos-col p1) (pos-col p2)))))
  (define (pos>? p1 p2)
    (or (fx> (pos-row p1) (pos-row p2))
        (and (fx= (pos-row p1) (pos-row p2))
             (fx> (pos-col p1) (pos-col p2)))))
  (define (pos>=? p1 p2)
    (or (fx> (pos-row p1) (pos-row p2))
        (and (fx= (pos-row p1) (pos-row p2))
             (fx>= (pos-col p1) (pos-col p2)))))
  (define (index->pos s n r c)
   ; convert index in single-string representation of entry
   ; into pos.  r and c are row and col at which string
   ; starts in the entry
    (let f ([i 0] [r r] [c c])
      (if (fx= i n)
          (make-pos r c)
          (if (char=? (string-ref s i) #\newline)
              (f (fx+ i 1) (fx+ r 1) 0)
              (f (fx+ i 1) r (fx+ c 1))))))
  (record-writer (type-descriptor pos)
    (lambda (x p wr)
      (fprintf p "#<pos ~a ~a>" (pos-row x) (pos-col x))))
)

(define lpchar #\()
(define rpchar #\))
(define lbchar #\[)
(define rbchar #\])

(define beep
  (lambda (str . arg*)
    #;(with-output-to-file "/tmp/ee.log"
        (lambda () (apply printf str arg*) (newline))
        'append)
    (when (ee-noisy) (bell))))

(module (string->entry
         entry->string
         string->lines
        ; primtiive and derived record accessors and mutators: no ee argument
         entry-col
         entry-nsr
         entry-row
         entry-mark
         entry-point
         null-entry?
         entry-mark-set!
         entry-row-set!
         entry-col-set!
        ; normal entry procedures: first two arguments are ee and entry
         add-char
         beginning-of-line?
         clear-entry
         id-completions
         correct&flash-matching-delimiter
         yank-entry
         delete-char
         delete-forward
         delete-to-eol
         echo-entry
         end-of-line?
         find-matching-delimiter
         find-next-sexp-backward
         find-next-sexp-forward
         find-next-word
         find-previous-word
         first-line?
         flash
         goto
         handle-winch
         indent
         indent-all
         insert-string-before
         insert-strings-before
         join-rows
         last-line?
         last-line-displayed?
         move-bol
         move-down
         move-eoe
         move-eol
         move-left
         move-right
         move-up
         only-whitespace-left?
         page-down
         page-up
         redisplay
         should-auto-indent?)

  ; NB. top-line and bot-line aren't really positions.
  ; the row does identify the logical row, but the col identifies the
  ; physical row of the logical row, i.e., 0 for the first physical
  ; row, 1 for the second, etc.

  (define-record-type entry
    (fields
      (immutable lns)               ; logical lines
      (mutable row)                 ; point (logical cursor) row
      (mutable col)                 ; point (logical cursor) column
      (mutable screen-cols)         ; cached screen columns
      (mutable screen-rows)         ; cached screen rows
      (mutable top-line)            ; first displayed line
      (mutable bot-line)            ; last displayed line
      (mutable mark))               ; current mark pos
    (nongenerative)
    (sealed #t)
    (protocol
      (lambda (new)
        (lambda (lns)
          (new lns 0 0 (screen-cols) (screen-rows)
            (make-pos 0 0) (make-pos 0 0) #f)))))

  (module ()
    (record-writer (type-descriptor entry)
      (lambda (x p wr)
        (display "#<ee entry>" p))))

  (define (entry-point entry)
    (make-pos (entry-row entry) (entry-col entry)))

  ;;; an lns is a nonempty list of logical lines, each of which may span
  ;;; multiple screen lines.  each line consists of an integer that records
  ;;; the number of screen rows spanned along with a string containing
  ;;; the text of the line.  lines are implicitly separated by newlines; no
  ;;; newlines appear in the strings themselves.
  ;;;
  ;;; lns   := (ln ln ...)  ;;; list of "ln"s
  ;;; ln    := [nsr, str]
  ;;; nsr   := integer      ;;; number of screen rows occupied by the line
  ;;; str   := string       ;;; contents of the line

 ; arrange for nsr to be updated whenever str is changed
  (module (make-ln ln? ln-str ln-nsr ln-str-set! ln-nsr-set!)
    (define-record-type (ln make-ln ln?)
      (fields
        (mutable str ln-str $ln-str-set!)
        (mutable nsr ln-nsr ln-nsr-set!))
      (nongenerative)
      (sealed #t)
      (protocol
        (lambda (new)
          (lambda (ee str)
            (new str (str->nsr ee str))))))
    (define (ln-str-set! ee ln str)
      ($ln-str-set! ln str)
      (ln-nsr-set! ln (str->nsr ee str))))

 ; extract nsr or str from selected row of lns
  (define (lns->nsr lns row) (ln-nsr (list-ref lns row)))
  (define (lns->str lns row) (ln-str (list-ref lns row)))

 ; replace str in selected row of lns
  (define (lns->str! ee lns row str)
    (ln-str-set! ee (list-ref lns row) str))

  (define (lns->char lns row col)
    (let ([str (lns->str lns row)])
      (if (fx< col (string-length str))
          (string-ref str col)
          #f)))

  (define (yank-entry ee entry)
    (map ln-str (entry-lns entry)))

  (define (entry->string entry)
    (let* ([lns (entry-lns entry)] [n (length lns)])
      (let ([sop (open-output-string)])
        (let loop ([i 0] [sep ""])
          (unless (fx= i n)
            (fprintf sop "~a~a" sep (lns->str lns i))
            (loop (fx+ i 1) "\n")))
        (get-output-string sop))))

  (define (echo-entry ee entry tp)
    (display-string (eestate-prompt ee) tp)
    (let ([lns (entry-lns entry)])
      (fprintf tp "~a\n" (ln-str (car lns)))
      (for-each
        (let ([pad (phantom-prompt ee)])
          (lambda (ln) (fprintf tp "~a~a\n" pad (ln-str ln))))
        (cdr lns)))
    (flush-output-port tp))

  (define (string->lines s)
   ; break string into list of lines while expanding tabs
    (let ([n (string-length s)] [op (open-output-string)])
      (let f ([i 0] [col 0])
        (if (fx= i n)
            (list (get-output-string op))
            (let ([c (string-ref s i)])
              (case c
                [(#\newline)
                 (let ([line (get-output-string op)])
                   (cons line (f (fx+ i 1) 0)))]
                [(#\tab)
                 (do ([i (fx- 8 (fxmodulo col 8)) (fx- i 1)])
                     ((fx= i 0))
                   (write-char #\space op))
                 (f (fx+ i 1) 0)]
                [(#\return) (f (fx+ i 1) col)]
                [else (write-char c op) (f (fx+ i 1) (fx+ col 1))]))))))

  (define (string->entry ee s)
    (let ([ln* (map (lambda (str) (make-ln ee str)) (string->lines s))])
      (make-entry (if (null? ln*) (list (make-ln ee "")) ln*))))

  (define (null-entry? entry)
    (let ([lns (entry-lns entry)])
      (and (fx= (length lns) 1)
           (equal? (ln-str (car lns)) ""))))

  (define (entry-nsr entry) (apply fx+ (map ln-nsr (entry-lns entry))))

  ;;; split a logical row into a list of strings each of which will fit
  ;;; on a screen line, starting at logical column col.
  (define split-string
    (lambda (ee str col)
      (let ([str-len (string-length str)])
        (let f ([col col] [width (fx- (screen-cols) (col->screen-col ee col))])
          (if (fx< (fx- str-len col) width)
              (list (substring str col str-len))
              (cons (substring str col (fx+ col width))
                    (f (fx+ col width) (screen-cols))))))))

  (define (screen-lines-between ee entry toprow topoff nextrow nextoff)
   ; returns distance in physical screen lines between physical line
   ; topoff of toprow and nextoff of nextrow
    (let ([lns (entry-lns entry)])
      (let f ([i toprow] [off topoff] [lns (list-tail lns toprow)])
        (if (fx= i nextrow)
            (fx- nextoff off)
            (fx+ (fx- (ln-nsr (car lns)) off)
                 (f (fx+ i 1) 0 (cdr lns)))))))

  (define (str->nsr ee str)
    (fx+ (col->line-offset ee (string-length str)) 1))

  ;;; return the line offset based on the column and screen size
  ;;;                                      ||-offset=2 (prompt)
  ;;; example: if: col         = 15        vv
  ;;;              offset      = 2         -----------
  ;;;              scrn-cols   = 10       |> line-000| line-offset 0
  ;;;          then:                      |line-11111| line-offset 1
  ;;;              line-offset = 1                ^column = 15
  (define col->line-offset
    (lambda (ee col)
      (fxquotient (fx+ (string-length (eestate-prompt ee)) col) (screen-cols))))

  ;;; return the actual screen column based on the logical row column
  ;;; example: if: col         = 15        vv-offset=2 (prompt)
  ;;;              offset      = 2         -----------
  ;;;              scrn-cols   = 10       |> line-000| line-offset 0
  ;;;          then:                      |line-11111| line-offset 1
  ;;;              scrn-col    = 7                ^column = 15
  (define col->screen-col
    (lambda (ee col)
      (fxremainder (fx+ col (string-length (eestate-prompt ee))) (screen-cols))))

  (define (clear-entry ee entry)
   ; like clear-screen, but clears only from top line of entry
    (if (visible? ee entry 0 0)
        (begin
          (carriage-return)
          (move-cursor-up
            (let ([top-line (entry-top-line entry)])
              (screen-lines-between ee entry
                (pos-row top-line) (pos-col top-line)
                (entry-row entry) (col->line-offset ee (entry-col entry)))))
          (clear-eos))
        (clear-screen)))

  ;;; given bottom line displayed, determines top line that will fill
  ;;; the screen to the extent possible
  (defopt (calc-top-line-displayed entry last-row-pos [nrows (screen-rows)])
    (let ([lns (entry-lns entry)])
      (let loop ([n nrows]
                 [r (pos-row last-row-pos)]
                 [off (pos-col last-row-pos)])
        (if (or (fx= n 1) (and (fx= r 0) (fx= off 0)))
            (make-pos r off)
            (if (fx= off 0)
                (loop (fx- n 1) (fx- r 1) (fx- (lns->nsr lns (fx- r 1)) 1))
                (loop (fx- n 1) r (fx- off 1)))))))

  ;;; given first line displayed, determines bottom line that will fill
  ;;; the screen to the extent possible
  (defopt (calc-bot-line-displayed entry first-row-pos [nrows (screen-rows)])
    (let* ([lns (entry-lns entry)]
           [last-row (fx- (length lns) 1)]
           [last-off (fx- (lns->nsr lns last-row) 1)]
           [first-row (pos-row first-row-pos)])
      (let loop ([n nrows]
                 [r first-row]
                 [off (pos-col first-row-pos)]
                 [off-max (fx- (lns->nsr lns first-row) 1)])
        (if (or (fx= n 1) (and (fx= r last-row) (fx= off last-off)))
            (make-pos r off)
            (if (fx= off off-max)
                (loop (fx- n 1) (fx+ r 1) 0 (fx- (lns->nsr lns (fx+ r 1)) 1))
                (loop (fx- n 1) r (fx+ off 1) off-max))))))

 ; NB.  the macos x terminal app distinguishes between empty screen
 ; positions (e.g., after clr_eos or clr_eol) and screen positions filled
 ; with spaces.  attempts to move past and clear after the former result
 ; in strange behavior.  (For example, the sequence clr_eos, cursor_right,
 ; clr_eol, 'a', clr_eol, and 'b' doesn't print the b but does cause the
 ; terminal to send back some characters.  Using ' ' in place of the
 ; cursor_right works as expected.)  For this reason, we display spaces
 ; and avoid using move-cursor-right to pad the front of each row after
 ; the first, which gets the actual prompt.

  (define (phantom-prompt ee)
    (make-string (string-length (eestate-prompt ee)) #\space))

  (module (display-rest/goto)
    (define (display-rest-of-line ee entry row col clear?)
     ; display as much of the rest of row as will fit on the screen
      (let ([lns (entry-lns entry)] [bot-line (entry-bot-line entry)])
       ; n = number of lines to display beyond the first
        (let loop ([n (fx- (if (fx= row (pos-row bot-line))
                               (pos-col bot-line)
                               (fx- (lns->nsr lns row) 1))
                           (col->line-offset ee col))]
                   [str-lst (split-string ee (lns->str lns row) col)]
                   [new-col col])
          (when clear? (clear-eol))
          (let ([str (car str-lst)])
            (ee-display-string (car str-lst))
            (let ([new-col (fx+ new-col (string-length str))])
              (if (fx= n 0)
                  new-col
                  (begin
                    (carriage-return)
                    (line-feed)
                    (loop (fx- n 1) (cdr str-lst) new-col))))))))
  
    (define (display-rest-of-entry ee entry)
      (let ([row (entry-row entry)]
            [col (entry-col entry)]
            [bot-row (pos-row (entry-bot-line entry))])
        (let loop ([new-row row] [start-col col])
          (let ([new-col (display-rest-of-line ee entry new-row start-col #f)])
            (if (fx= new-row bot-row)
                (values new-row new-col)
                (begin
                  (carriage-return)
                  (line-feed)
                  (ee-display-string (phantom-prompt ee))
                  (loop (fx+ new-row 1) 0)))))))
  
    (define (display-rest/goto ee entry just-row? clear? to-row to-col)
     ; display rest of entry and go directly from there to (to-row, to-col)
     ; just-row? => only remainder of current logical row needed by displayed
     ; clear? => clear-eos or clear-eol needed
      (let-values ([(cur-row cur-col)
                    (if just-row?
                        (values
                          (entry-row entry)
                          (display-rest-of-line ee entry
                            (entry-row entry) (entry-col entry) clear?))
                        (begin
                          (entry-bot-line-set! entry
                            (calc-bot-line-displayed entry
                              (entry-top-line entry)))
                          (when clear? (clear-eos))
                          (display-rest-of-entry ee entry)))])
        (unless (and (fx= cur-row (entry-row entry))
                     (fx= cur-col (entry-col entry)))
          (entry-row-set! entry cur-row)
         ; if the last character written was in the last column of a screen
         ; line, move back one so that the cursor is pointing at that character
         ; to avoid returning a column value that would wrongly indicate that
         ; the cursor is at the start of the next screen line
          (if (and (fx> cur-col 0) (fx= (col->screen-col ee cur-col) 0))
              (begin
                (move-cursor-left 1)
                (entry-col-set! entry (fx- cur-col 1)))
              (entry-col-set! entry cur-col)))
        (goto ee entry (make-pos to-row to-col)))))

  (module (display-partial-entry)
    (define (display-partial-row ee row str start end)
     ; displays physical lines of str from start (inclusive) to end (inclusive)
     ; assumes cursor is at column zero of start line; leaves cursor at
     ; column zero of end line
      (let ([ls (list-tail (split-string ee str 0) start)])
        (when (fx= start 0)
          (ee-display-string
            (if (fx= row 0)
                (eestate-prompt ee)
                (phantom-prompt ee))))
        (ee-display-string (car ls))
        (carriage-return)
        (do ([i start (fx+ i 1)] [ls (cdr ls) (cdr ls)])
            ((fx= i end))
          (line-feed)
          (ee-display-string (car ls))
          (carriage-return))))

    (define (display-partial-entry ee entry toprow topoff botrow botoff)
     ; displays physical screen lines between physical line topoff of
     ; toprow (inclusive) and botoff of botrow (inclusive)
     ; assumes cursor is at column zero of first physical line to be displayed;
     ; leaves cursor at column zero of last line displayed
      (let ([lns (entry-lns entry)])
        (let loop ([r toprow] [start topoff] [lns (list-tail lns toprow)])
          (display-partial-row ee r (ln-str (car lns)) start
            (if (fx= r botrow) botoff (fx- (ln-nsr (car lns)) 1)))
          (unless (fx= r botrow)
            (line-feed)
            (loop (fx+ r 1) 0 (cdr lns)))))))

  (define (goto-backward ee entry new-row new-col)
    (assert* (fx>= new-row 0)
             (fx>= new-col 0)
             (fx<= new-col (string-length (lns->str (entry-lns entry) new-row))))
    (let* ([lns (entry-lns entry)]
           [row (entry-row entry)]
           [col (entry-col entry)]
           [top-line (entry-top-line entry)]
           [new-str (lns->str lns new-row)]
           [new-len (string-length new-str)]
           [new-row-offset (col->line-offset ee new-col)]
           [new-row-pos (make-pos new-row new-row-offset)]
           [new-bot-line (calc-bot-line-displayed entry new-row-pos)])
      (cond
       ; case 1: destination on screen, no scrolling necessary
       ;  ------------------
       ; | (define fact     | <--top-line
       ; |   (lambda (n)    | <--new-row
       ; |     (if (zero? n)| <--point
       ; |         1        |
       ; |         (* n (fac|
       ; |t (sub1 n))))))   | <--bot-line
       ; |                  |
       ;  ------------------
        [(pos>=? new-row-pos (entry-top-line entry))
         (move-cursor-up
           (screen-lines-between ee entry
             new-row new-row-offset
             (entry-row entry) (col->line-offset ee (entry-col entry))))
         (let ([screen-col (col->screen-col ee col)]
               [new-screen-col (col->screen-col ee new-col)])
           (cond
             [(fx> new-screen-col screen-col)
              (move-cursor-right (fx- new-screen-col screen-col))]
             [(fx< new-screen-col screen-col)
              (move-cursor-left (fx- screen-col new-screen-col))]))
         (entry-row-set! entry new-row)
         (entry-col-set! entry new-col)]

       ; case 2: a portion of the old screen overlaps the new screen.
       ;         we will scroll down and keep the overlap instead of
       ;         redrawing
       ;         + = new screen border
       ;         - = old-screen border
       ;  ++++++++++++++++++
       ; | (define f        | <--new-row        0         }extra-top-
       ; |   (lambda (n)    |                             }lines
       ;  ------------------
       ; |     (if (zero? n)| <--top-line       (2 . 0)
       ; |         1        | <--point          (row . col)
       ; |         (* n     | <--new-bot-line   (4 . 0)
       ;  ++++++++++++++++++
       ; |            (f    | <--bot-line       (5 . 0)
       ; |              (1- |
       ;  ------------------
       ;                  n))))))
        [(pos>? new-bot-line (entry-top-line entry))
        ; move cursor to physical column 0 of top screen line
         (move-cursor-up
           (screen-lines-between ee entry
             (pos-row top-line) (pos-col top-line)
             row (col->line-offset ee col)))
         (carriage-return)
         (let ([extra-top-lines
                (screen-lines-between ee entry
                  new-row new-row-offset
                  (pos-row top-line) (pos-col top-line))])
          ; reverse scroll to open up space at the top
          ; if we're not actually at the top of the physical display, e.g.,
          ; if we only partially displayed the entry after an error or tab-tab,
          ; we hope that this goes up a line and clears to end of line.  if
          ; this ever gives us problems, we'll have avoid getting into this
          ; case when less than a screenful of lines has been displayed.
           (scroll-reverse extra-top-lines)
          ; display the extra lines
           (let ([r (pos-row top-line)] [off (fx- (pos-col top-line) 1)])
             (if (fx>= off 0)
                 (display-partial-entry ee entry new-row new-row-offset r off)
                 (display-partial-entry ee entry new-row new-row-offset
                   (fx- r 1) (fx- (lns->nsr lns (fx- r 1)) 1))))
          ; move cursor back to top
           (move-cursor-up (fx- extra-top-lines 1)))
         (move-cursor-right (col->screen-col ee new-col))
         (entry-col-set! entry new-col)
         (entry-row-set! entry new-row)
         (entry-top-line-set! entry new-row-pos)
         (when (pos<? new-bot-line (entry-bot-line entry))
           (entry-bot-line-set! entry new-bot-line))]

       ; case 3: no overlap between old screen area and new screen
       ;         area. we will redraw the entire screen
       ; + = new screen border
       ; - = old-screen border
       ;  ++++++++++++++++++
       ; | (define f        | <--new-row        0
       ; |   (lambda (n)    |
       ; |     (if (zero? n)| <--new-bot-line   (4 . 0)
       ;  ++++++++++++++++++
       ;  ------------------
       ; |         1        | <--top-line       (2 . 0)
       ; |         (* n     |
       ; |            (f    | <--bot-line       (4 . 1)
       ;  ------------------
       ;                (1-
       ;                  n))))))
        [else
         (clear-screen)
         (display-partial-entry ee entry
           new-row new-row-offset
           (pos-row new-bot-line) (pos-col new-bot-line))
         (move-cursor-up
           (screen-lines-between ee entry
             new-row new-row-offset
             (pos-row new-bot-line) (pos-col new-bot-line)))
         (move-cursor-right (col->screen-col ee new-col))
         (entry-col-set! entry new-col)
         (entry-row-set! entry new-row)
         (entry-top-line-set! entry new-row-pos)
         (entry-bot-line-set! entry new-bot-line)])))

  (define (goto-forward ee entry new-row new-col)
    (assert* (fx< new-row (length (entry-lns entry)))
             (fx>= new-col 0)
             (fx<= new-col (string-length (lns->str (entry-lns entry) new-row))))
    (let* ([lns (entry-lns entry)]
           [row (entry-row entry)]
           [col (entry-col entry)]
           [bot-line (entry-bot-line entry)]
           [new-str (lns->str lns new-row)]
           [new-len (string-length new-str)]
           [new-row-offset (col->line-offset ee new-col)]
           [new-row-pos (make-pos new-row new-row-offset)]
           [new-top-line (calc-top-line-displayed entry new-row-pos)])
      (cond
       ; case 1: destination on screen, no scrolling necessary
       ;  ------------------
       ; | (define fact     | <--top-line
       ; |   (lambda (n)    | <--point
       ; |     (if (zero? n)| <--new-row
       ; |         1        |
       ; |         (* n (fac|
       ; |t (sub1 n))))))   | <--bot-line
       ; |                  |
       ;  ------------------
        [(pos<=? new-row-pos bot-line)
         (move-cursor-down
           (screen-lines-between ee entry
             row (col->line-offset ee col)
             new-row new-row-offset))
         (let ([screen-col (col->screen-col ee col)]
               [new-screen-col (col->screen-col ee new-col)])
           (cond
             [(fx> new-screen-col screen-col)
              (move-cursor-right (fx- new-screen-col screen-col))]
             [(fx< new-screen-col screen-col)
              (move-cursor-left (fx- screen-col new-screen-col))]))
         (entry-row-set! entry new-row)
         (entry-col-set! entry new-col)]

       ; case 2: a portion of the old screen overlaps the new screen.
       ;         we will scroll up and keep the overlap
       ;
       ;         + = new screen border
       ;         - = old-screen border
       ;  ------------------
       ; | (define f        | <--top-line       (0 . 0)
       ; |   (lambda (n)    |
       ;  ++++++++++++++++++
       ; |     (if (zero? n)| <--new-top-line               } scrn-
       ; |         1        | <--point          (row . col) } draw-
       ; |         (* n     | <--bot-line       (4 . 0)     } lines
       ;  ------------------
       ; |            (f    |
       ; |              (1- | <--new-row        6
       ;  ++++++++++++++++++
       ;                  n))))))
        [(pos>=? bot-line new-top-line)
        ; move cursor to physical col 0 of first line after old bot-line
         (move-cursor-down
           (screen-lines-between ee entry
             row (col->line-offset ee col)
             (pos-row bot-line) (pos-col bot-line)))
         (carriage-return)
         (line-feed)
         (let ([r (pos-row bot-line)] [off (fx+ (pos-col bot-line) 1)])
           (if (fx< off (lns->nsr lns r))
               (display-partial-entry ee entry r off
                 new-row new-row-offset)
               (display-partial-entry ee entry (fx+ r 1) 0
                 new-row new-row-offset)))
         (move-cursor-right (col->screen-col ee new-col))
         (entry-col-set! entry new-col)
         (entry-row-set! entry new-row)
         (when (pos>? new-top-line (entry-top-line entry))
           (entry-top-line-set! entry new-top-line))
         (entry-bot-line-set! entry new-row-pos)]

       ; case 3: no overlap between old screen area and new screen
       ;         area. we will redraw the entire screen
       ; + = new screen border
       ; - = old-screen border
       ;  ++++++++++++++++++
       ; | (define f        | <--top-line       (0 . 0)
       ; |   (lambda (n)    |
       ; |     (if (zero? n)| <--bot-line       (2 . 0)
       ;  ++++++++++++++++++
       ;  ------------------
       ; |         1        | <--new-top-line
       ; |         (* n     |
       ; |            (f    | <--new-row, new-row-offset
       ;  ------------------
       ;                (1-
       ;                  n))))))
        [else
         (clear-screen)
         (display-partial-entry ee entry
           (pos-row new-top-line) (pos-col new-top-line)
           new-row new-row-offset)
         (move-cursor-right (col->screen-col ee new-col))
         (entry-col-set! entry new-col)
         (entry-row-set! entry new-row)
         (entry-top-line-set! entry new-top-line)
         (entry-bot-line-set! entry new-row-pos)])))

  (define (goto ee entry p)
    (let ([new-row (pos-row p)] [new-col (pos-col p)])
      (assert* (fx< new-row (length (entry-lns entry)))
               (fx<= new-col (string-length (lns->str (entry-lns entry) new-row))))
      (if (or (fx< new-row (entry-row entry))
              (and (fx= new-row (entry-row entry))
                   (fx< new-col (entry-col entry))))
          (goto-backward ee entry new-row new-col)
          (goto-forward ee entry new-row new-col))))

  (defopt (move-up ee entry [n 1])
    (assert* (fx>= (fx- (entry-row entry) n) 0))
    (let ([new-row (fx- (entry-row entry) n)])
      (goto-backward ee entry new-row
        (fxmin (entry-col entry)
               (string-length (lns->str (entry-lns entry) new-row))))))

  (defopt (move-down ee entry [n 1])
    (assert* (fx< (fx+ (entry-row entry) n) (length (entry-lns entry))))
    (let ([new-row (fx+ (entry-row entry) n)])
      (goto-forward ee entry new-row
        (fxmin (entry-col entry)
               (string-length (lns->str (entry-lns entry) new-row))))))

  (defopt (move-left ee entry [n 1])
    (let ([new-col (fx- (entry-col entry) n)])
      (assert* (fx>= new-col 0))
      (goto-backward ee entry (entry-row entry) new-col)))

  (defopt (move-right ee entry [n 1])
    (let ([new-col (fx+ (entry-col entry) n)])
      (assert* (fx<= new-col (string-length (lns->str (entry-lns entry) (entry-row entry)))))
      (goto-forward ee entry (entry-row entry) new-col)))

  (define (page-down ee entry)
    (let* ([last-row (fx- (length (entry-lns entry)) 1)]
           [row (entry-row entry)]
           [col (entry-col entry)]
           [point-line-offset (col->line-offset ee col)]
           [top-line (entry-top-line entry)]
           [bot-line (entry-bot-line entry)]
           [n (screen-lines-between ee entry
                (pos-row top-line) (pos-col top-line)
                (pos-row bot-line) (pos-col bot-line))])
        (let f ([r (fxmin (fx+ row n) last-row)])
          (if (fx= r row)
              (unless (fx= r last-row)
                (goto-forward ee entry (fx+ r 1)
                  (fxmin col (string-length (lns->str (entry-lns entry) (fx+ r 1))))))
              (let ([c (fxmin col (string-length (lns->str (entry-lns entry) r)))])
                (if (<= (screen-lines-between ee entry
                          row point-line-offset
                          r (col->line-offset ee c))
                        n)
                    (goto-forward ee entry r c)
                    (f (fx- r 1))))))))

  (define (page-up ee entry)
    (let* ([row (entry-row entry)]
           [col (entry-col entry)]
           [point-line-offset (col->line-offset ee col)]
           [top-line (entry-top-line entry)]
           [bot-line (entry-bot-line entry)]
           [n (screen-lines-between ee entry
                (pos-row top-line) (pos-col top-line)
                (pos-row bot-line) (pos-col bot-line))])
        (let f ([r (max (fx- row n) 0)])
          (if (fx= r row)
              (unless (fx= r 0)
                (goto-backward ee entry (fx- r 1)
                  (fxmin col (string-length (lns->str (entry-lns entry) (fx- r 1))))))
              (let ([c (fxmin col (string-length (lns->str (entry-lns entry) r)))])
                (if (<= (screen-lines-between ee entry
                          r (col->line-offset ee c)
                          row point-line-offset)
                        n)
                    (goto-backward ee entry r c)
                    (f (fx+ r 1))))))))

  (define (move-eol ee entry)
    (move-right ee entry
      (fx- (string-length (lns->str (entry-lns entry) (entry-row entry)))
           (entry-col entry))))

  (define (move-bol ee entry)
    (move-left ee entry (entry-col entry)))

  (define (move-eoe ee entry)
    (let ([lns (entry-lns entry)])
      (let ([r (fx- (length lns) 1)])
        (goto-forward ee entry r (string-length (lns->str lns r))))))

  (define (move-to-col-pos ee entry new-col)
    (let ([col (entry-col entry)])
      (if (fx< new-col col)
          (move-left ee entry (fx- col new-col))
          (move-right ee entry (fx- new-col col)))))

  (define (adjust-mark/delete ee entry r1 c1 r2 c2)
    (let ([mark (entry-mark entry)])
      (when mark
        (let ([mrow (pos-row mark)] [mcol (pos-col mark)])
          (unless (or (fx< mrow r1) (and (fx= mrow r1) (fx< mcol c1)))
            (entry-mark-set! entry
              (and (not (or (fx< mrow r2) (and (fx= mrow r2) (fx< mcol c2))))
                   (make-pos
                     (fx- mrow (fx- r2 r1))
                     (if (fx= mrow r2) (fx+ c1 (fx- mcol c2)) mcol)))))))))

  (define (adjust-mark/insert ee entry r1 c1 r2 c2)
    (let ([mark (entry-mark entry)])
      (when mark
        (let ([mrow (pos-row mark)] [mcol (pos-col mark)])
          (unless (or (fx< mrow r1) (and (fx= mrow r1) (fx< mcol c1)))
            (entry-mark-set! entry
              (make-pos
                (fx+ mrow (fx- r2 r1))
                (if (fx= mrow r1) (fx+ c2 (fx- mcol c1)) mcol))))))))

  (define (delete-forward ee entry r2 c2)
   ; deletes from point, aka r1, c1 (inclusive) to r2, c2 (exclusive)
   ; and returns the deleted content as a list of strings
    (let ([r1 (entry-row entry)] [c1 (entry-col entry)])
      (assert* (or (fx< r1 r2) (and (fx= r1 r2) (fx<= c1 c2))))
      (adjust-mark/delete ee entry r1 c1 r2 c2)
      (if (fx= r1 r2)
          (let* ([ln (list-ref (entry-lns entry) r1)]
                 [s (ln-str ln)]
                 [old-nsr (ln-nsr ln)])
            (ln-str-set! ee ln
              (string-append
                (substring s 0 c1)
                (substring s c2 (string-length s))))
            (display-rest/goto ee entry (fx= (ln-nsr ln) old-nsr) #t r1 c1)
            (list (substring s c1 c2)))
          (let* ([lns (entry-lns entry)]
                 [ls1 (list-tail lns r1)]
                 [ls2 (list-tail ls1 (fx- r2 r1))]
                 [s1 (ln-str (car ls1))]
                 [s2 (ln-str (car ls2))])
            (ln-str-set! ee (car ls1)
              (string-append
                (substring s1 0 c1)
                (substring s2 c2 (string-length s2))))
            (let ([deleted
                   (cons (substring s1 c1 (string-length s1))
                         (let f ([ls (cdr ls1)])
                           (if (eq? ls ls2)
                               (list (substring s2 0 c2))
                               (cons (ln-str (car ls)) (f (cdr ls))))))])
              (set-cdr! ls1 (cdr ls2))
              (display-rest/goto ee entry #f #t r1 c1)
              deleted)))))

  (define (delete-char ee entry)
    (assert* (not (end-of-line? ee entry)))
    (let ([row (entry-row entry)] [col (entry-col entry)])
      (delete-forward ee entry row (fx+ col 1))))

  (define (delete-to-eol ee entry)
    (let ([row (entry-row entry)])
      (delete-forward ee entry row
        (string-length (lns->str (entry-lns entry) row)))))

  (define (join-rows ee entry)
    (assert* (end-of-line? ee entry) (not (last-line? ee entry)))
    (delete-forward ee entry (fx+ (entry-row entry) 1) 0))

  (define (insert-string-before ee entry new-str)
    (let* ([row (entry-row entry)]
           [col (entry-col entry)]
           [lns (entry-lns entry)]
           [ln (list-ref lns row)]
           [str (ln-str ln)]
           [str-len (string-length str)]
           [new-col (fx+ col (string-length new-str))]
           [nsr (ln-nsr ln)]
           [eoe? (end-of-entry? ee entry)])
      (ln-str-set! ee ln
        (string-append
          (substring str 0 col)
          new-str
          (substring str col (string-length str))))
      (let ([just-row? (fx= (ln-nsr ln) nsr)])
        (display-rest/goto ee entry just-row?
         ; avoid clear-eol/eos if insertion takes place at end of entry or
         ; if rewriting just the current row
          (and (not eoe?) (not just-row?))
          row new-col))
      (adjust-mark/insert ee entry row col row new-col)))

  (define (add-char ee entry c)
   ; add character after point, then move point forward one character
    (assert* (char? c))
    (insert-string-before ee entry (string c)))

  (define (insert-strings-before ee entry strs)
    (unless (null? strs)
      (if (fx= (length strs) 1)
          (insert-string-before ee entry (car strs))
          (let* ([row (entry-row entry)]
                 [col (entry-col entry)]
                 [lns (entry-lns entry)]
                 [ls (list-tail lns row)]
                 [ln (car ls)]
                 [point-str (ln-str ln)]
                 [eoe? (end-of-entry? ee entry)])
            (ln-str-set! ee ln
              (string-append (substring point-str 0 col) (car strs)))
            (set-cdr! ls
              (let f ([str (cadr strs)] [strs (cddr strs)])
                (if (null? strs)
                    (cons (make-ln ee
                            (string-append str
                              (substring point-str col
                                (string-length point-str))))
                          (cdr ls))
                    (cons (make-ln ee str)
                          (f (car strs) (cdr strs))))))
            (let* ([n (fx- (length strs) 1)]
                   [new-row (fx+ row n)]
                   [new-col (string-length (list-ref strs n))])
              (display-rest/goto ee entry #f (not eoe?) new-row new-col)
              (adjust-mark/insert ee entry row col new-row new-col))))))

  (define (first-line? ee entry) (fxzero? (entry-row entry)))

  (define (last-line? ee entry)
    (fx= (entry-row entry) (fx1- (length (entry-lns entry)))))

  (define (last-line-displayed? ee entry)
    (pos=? (make-pos (entry-row entry) (col->line-offset ee (entry-col entry)))
           (entry-bot-line entry)))

  (define (visible? ee entry row col)
    (let ([line (make-pos row (col->line-offset ee col))])
      (and (pos<=? (entry-top-line entry) line)
           (pos<=? line (entry-bot-line entry)))))

  (define (end-of-line? ee entry)
    (fx= (entry-col entry)
         (string-length (lns->str (entry-lns entry) (entry-row entry)))))

  (define (end-of-entry? ee entry)
    (and (fx= (entry-row entry) (fx- (length (entry-lns entry)) 1))
         (end-of-line? ee entry)))

  (define (beginning-of-line? ee entry) (fx= (entry-col entry) 0))

 ; returns #t iff only spaces and newlines are left after point
  (define (only-whitespace-left? ee entry)
    (let f ([ls (list-tail (entry-lns entry) (entry-row entry))]
            [col (entry-col entry)])
      (or (null? ls)
          (let* ([s (ln-str (car ls))] [n (string-length s)])
            (let g ([col col])
              (if (fx= col n)
                  (f (cdr ls) 0)
                  (and (char=? (string-ref s col) #\space)
                       (g (fx+ col 1)))))))))

  (define (handle-winch ee entry)
    (screen-resize!)
    (unless (and (fx= (screen-rows) (entry-screen-rows entry))
                 (fx= (screen-cols) (entry-screen-cols entry)))
      (clear-entry ee entry)
      (redisplay ee entry)))

  (module (redisplay)
    (define (set-screen-size! ee entry)
      (screen-resize!)
      (unless (and (fx= (entry-screen-cols entry) (screen-cols))
                   (fx= (entry-screen-rows entry) (screen-rows)))
        (for-each
          (lambda (ln) (ln-nsr-set! ln (str->nsr ee (ln-str ln))))
          (entry-lns entry))
        (entry-screen-cols-set! entry (screen-cols))
        (entry-screen-rows-set! entry (screen-rows))))

    (defopt (redisplay ee entry [nrows #f])
      (set-screen-size! ee entry)
      (let* ([nrows (or nrows (screen-rows))] ; want new screen-rows
             [row (entry-row entry)]
             [col (entry-col entry)]
             [point-line (make-pos row (col->line-offset ee col))])
        (entry-bot-line-set! entry
          (calc-bot-line-displayed entry (entry-top-line entry) nrows))
        (when (pos>? point-line (entry-bot-line entry))
          (entry-bot-line-set! entry point-line))
        (entry-top-line-set! entry
          (calc-top-line-displayed entry (entry-bot-line entry) nrows))
        (when (pos<? point-line (entry-top-line entry))
          (entry-top-line-set! entry point-line)
          (entry-bot-line-set! entry
            (calc-bot-line-displayed entry (entry-top-line entry) nrows)))
        (let ([top-line (entry-top-line entry)] [bot-line (entry-bot-line entry)])
          (display-partial-entry ee entry
            (pos-row top-line) (pos-col top-line)
            (pos-row bot-line) (pos-col bot-line))
          (move-cursor-up
            (screen-lines-between ee entry
              (pos-row point-line) (pos-col point-line)
              (pos-row bot-line) (pos-col bot-line)))
          (move-cursor-right (col->screen-col ee col))))))

  (define (flash ee entry mpos)
    (let ([point-pos (entry-point entry)])
      (cond
        [(visible? ee entry (pos-row mpos) (pos-col mpos))
         (goto ee entry mpos)
         (ee-flush)
         (wait (ee-paren-flash-delay))
         (goto ee entry point-pos)]
        [(pos<? mpos point-pos)
         (let ([nlines
                (screen-lines-between ee entry
                  (pos-row (entry-top-line entry))
                  (pos-col (entry-top-line entry))
                  (pos-row point-pos)
                  (col->line-offset ee (pos-col point-pos)))]
               [ncols (col->screen-col ee (pos-col point-pos))])
           (move-cursor-left ncols)
           (move-cursor-up nlines)
           (ee-flush)
           (wait (ee-paren-flash-delay))
           (move-cursor-down nlines)
           (move-cursor-right ncols))]
        [else
         (let ([nlines
                (screen-lines-between ee entry
                  (pos-row point-pos)
                  (col->line-offset ee (pos-col point-pos))
                  (pos-row (entry-bot-line entry))
                  (pos-col (entry-top-line entry)))]
               [ncols (col->screen-col ee (pos-col point-pos))])
           (move-cursor-left ncols)
           (move-cursor-down nlines)
           (ee-flush)
           (wait (ee-paren-flash-delay))
           (move-cursor-up nlines)
           (move-cursor-right ncols))])))

  (define (correct&flash-matching-delimiter ee entry)
    (define (expected left) (if (eqv? left lbchar) rbchar rpchar))
    (move-left ee entry 1) ; move over delim
    (let ([lns (entry-lns entry)])
      (let* ([row (entry-row entry)]
             [col (entry-col entry)]
             [str (lns->str lns row)]
             [c (string-ref str col)])
        (if (or (char=? c lpchar) (char=? c lbchar))
           ; don't correct close delimiter when inserting open delimiter
           ; since doing so often leads to surprising results
            (when (ee-flash-parens)
              (cond
                [(find-matching-delim-forward ee entry row col #f) =>
                 (lambda (mpos) (flash ee entry mpos))]))
            (cond
              [(find-matching-delim-backward ee entry row col
                 (ee-auto-paren-balance)) =>
               (lambda (mpos)
                 (let ([cexp (expected
                               (string-ref
                                 (lns->str lns (pos-row mpos))
                                 (pos-col mpos)))])
                   (unless (eqv? c cexp)
                     (string-set! str col cexp)
                     (ee-write-char cexp)
                     (move-cursor-left 1)))
                 (when (ee-flash-parens) (flash ee entry mpos)))]))))
    (move-right ee entry 1))

  (define (find-matching-delimiter ee entry)
    (let ([row (entry-row entry)]
          [col (entry-col entry)]
          [str (lns->str (entry-lns entry) (entry-row entry))])
      (and (fx< col (string-length str))
           (let ([c (string-ref str col)])
             (if (or (char=? c lpchar) (char=? c lbchar))
                 (find-matching-delim-forward ee entry row col #f)
                 (and (or (char=? c rpchar) (char=? c rbchar))
                      (find-matching-delim-backward ee entry row col #f)))))))

  (define (find-matching-delim-backward ee entry row col lax?)
    (let ([lns (entry-lns entry)])
     ; 1. create string representing current entry through row, col
     ; 2. search forward, stacking left/right delimiters and their indices
     ; 3. if matching delimiter found, convert string index to pos
      (let* ([s (let ([op (open-output-string)])
                  (let loop ([i 0] [sep ""])
                    (let ([str (lns->str lns i)])
                      (if (= i row)
                          (fprintf op "~a~a" sep (substring str 0 (fx+ col 1)))
                          (begin
                            (fprintf op "~a~a" sep str)
                            (loop (fx+ i 1) "\n")))))
                  (get-output-string op))]
             [ip (open-input-string s)])
        (let loop ([stack '()])
          (on-error (loop '())
            (let-values ([(type value start end) (read-token ip)])
              (case type
                [(atomic box dot insert mark quote) (loop stack)]
                [(lbrack record-brack)
                 (loop (cons (cons 'rbrack end) stack))]
                [(lparen vfxnparen vfxparen vnparen vparen vu8nparen vu8paren)
                 (loop (cons (cons 'rparen end) stack))]
                [(rbrack rparen)
                 (if (= end (string-length s))
                     (and (not (null? stack))
                          (or lax? (eq? (caar stack) type))
                          (index->pos s (fx- (cdar stack) 1) 0 0))
                     (if (and (not (null? stack)) (eq? (caar stack) type))
                         (loop (cdr stack))
                         (loop '())))]
                [(eof fasl) #f]
                [else
                 (warningf 'expeditor "unexpected token type ~s from read-token" type)
                 #f])))))))

  (define (find-matching-delim-forward ee entry row col lax?)
    (let ([lns (entry-lns entry)])
     ; should be sitting on left paren or bracket
      (assert* (or (char=? (lns->char lns row col) lpchar)
                   (char=? (lns->char lns row col) lbchar)))
     ; 1. create string representing current entry starting at col, row
     ; 2. search forward until matching delimiter, eof, or error
     ; 3. if matching delimiter found, convert string index to pos
      (let* ([s (let ([op (open-output-string)] [l-lns (length lns)])
                  (let ([s (lns->str lns row)])
                    (display (substring s col (string-length s)) op))
                  (let loop ([i (fx+ row 1)])
                    (unless (fx= i l-lns)
                      (fprintf op "\n~a" (lns->str lns i))
                      (loop (fx+ i 1))))
                  (get-output-string op))]
             [ip (open-input-string s)])
        (on-error #f
          (let loop ([stack '()])
            (let-values ([(type value start end) (read-token ip)])
              (case type
                [(atomic box dot insert mark quote) (loop stack)]
                [(lbrack record-brack)
                 (loop (cons 'rbrack stack))]
                [(lparen vfxnparen vfxparen vnparen vparen vu8nparen vu8paren)
                 (loop (cons 'rparen stack))]
                [(rbrack rparen)
                 (if (fx= (length stack) 1)
                     (and (or lax? (eq? (car stack) type))
                          (index->pos s start row col))
                     (and (eq? (car stack) type) (loop (cdr stack))))]
                [(eof fasl) #f]
                [else
                 (warningf 'expeditor "unexpected token type ~s from read-token" type)
                 #f])))))))

  (define (find-next-sexp-backward ee entry row col)
    (let* ([lns (entry-lns entry)]
           [s (let ([op (open-output-string)])
                (let loop ([i 0] [sep ""])
                  (let ([str (lns->str lns i)])
                    (if (= i row)
                        (fprintf op "~a~a" sep (substring str 0 col))
                        (begin
                          (fprintf op "~a~a" sep str)
                          (loop (fx+ i 1) "\n")))))
                (get-output-string op))]
           [ip (open-input-string s)])
      (on-error #f
        (let loop ([stack '()] [last-start 0])
          (let-values ([(type value start end) (read-token ip)])
            (case type
              [(atomic dot insert mark)
               (if (and (not (null? stack)) (eq? (caar stack) 'qubx))
                   (loop (cdr stack) (cdar stack))
                   (loop stack start))]
              [(box quote)
               (if (and (not (null? stack)) (eq? (caar stack) 'qubx))
                   (loop stack #f)
                   (loop (cons (cons 'qubx start) stack) #f))]
              [(eof) (and last-start (index->pos s last-start 0 0))]
              [(lbrack record-brack)
               (if (and (not (null? stack)) (eq? (caar stack) 'qubx))
                   (loop (cons (cons 'rbrack (cdar stack)) (cdr stack)) #f)
                   (loop (cons (cons 'rbrack start) stack) #f))]
              [(lparen vfxnparen vfxparen vnparen vparen vu8nparen vu8paren)
               (if (and (not (null? stack)) (eq? (caar stack) 'qubx))
                   (loop (cons (cons 'rparen (cdar stack)) (cdr stack)) #f)
                   (loop (cons (cons 'rparen start) stack) #f))]
              [(rbrack rparen)
               (if (and (not (null? stack)) (eq? (caar stack) type))
                   (loop (cdr stack) (cdar stack))
                   (loop '() #f))]
              [else
               (warningf 'expeditor "unexpected token type ~s from read-token" type)
               #f]))))))

  (define (find-next-sexp-forward ee entry row col ignore-whitespace?)
   ; ordinarily stops at first s-expression if it follows whitespace (or
   ; comments), but always moves to second if ignore-whitespace? is true
    (let* ([lns (entry-lns entry)]
           [s (let ([op (open-output-string)] [l-lns (length lns)])
                   (let ([s (lns->str lns row)])
                     (display (substring s col (string-length s)) op))
                   (let loop ([i (fx+ row 1)])
                     (unless (fx= i l-lns)
                       (fprintf op "\n~a" (lns->str lns i))
                       (loop (fx+ i 1))))
                   (get-output-string op))]
           [ip (open-input-string s)])
      (define (skip start)
        (index->pos s
          (on-error start
            (let-values ([(type value start end) (read-token ip)])
              start))
          row col))
      (on-error #f
        (let loop ([stack '()] [first? #t] [ignore? #f])
          (let-values ([(type value start end) (read-token ip)])
            (if (and first? (not ignore-whitespace?) (fx> start 0))
                (and (not ignore?) (index->pos s start row col))
                (case type
                  [(atomic dot insert mark)
                   (if (null? stack)
                       (and (not ignore?) (skip start))
                       (loop stack #f ignore?))]
                  [(box) (loop stack #f ignore?)]
                  [(quote)
                   (when (and ignore-whitespace?
                              (eq? value 'datum-comment)
                              (null? stack))
                     (loop '() #f #t))
                   (loop stack #f ignore?)]
                  [(eof fasl) #f]
                  [(lbrack record-brack) (loop (cons 'rbrack stack) #f ignore?)]
                  [(lparen vfxnparen vfxparen vnparen vparen vu8nparen vu8paren)
                   (loop (cons 'rparen stack) #f ignore?)]
                  [(rbrack rparen)
                   (and (not (null? stack))
                        (eq? (car stack) type)
                        (let ([stack (cdr stack)])
                          (if (null? stack)
                              (and (not ignore?) (skip start))
                              (loop stack #f ignore?))))]
                  [else
                   (warningf 'expeditor "unexpected token type ~s from read-token" type)
                   #f])))))))

  (module (find-next-word find-previous-word)
    (define separator?
      (lambda (c)
        (memq c '(#\space #\; #\( #\) #\[ #\] #\" #\' #\`))))

    (define (find-next-word ee entry row col)
     ; always returns a position
      (let ([lns (entry-lns entry)])
       ; skip past separators
        (let loop ([row row] [col col])
          (cond
            [(fx= col (string-length (lns->str lns row)))
             (if (fx= row (fx1- (length lns)))
                 (make-pos row col)
                 (loop (fx1+ row) 0))]
            [(separator? (lns->char lns row col))
             (loop row (fx1+ col))]
           ; now we are past initial separators, find next separator
            [else
             (let loop ([col col])
               (cond
                 [(or (fx= col (string-length (lns->str lns row)))
                      (separator? (lns->char lns row col)))
                  (make-pos row col)]
                 [else (loop (fx1+ col))]))]))))

    (define (find-previous-word ee entry row col)
     ; always returns a position
      (let ([lns (entry-lns entry)])
       ; skip past separators space (starts at char left of current)
        (let loop ([row row] [col col])
          (cond
            [(fx= col 0)
             (if (fx= row 0)
                 (make-pos row col)
                 (loop
                   (fx1- row)
                   (string-length (lns->str lns (fx1- row)))))]
            [(separator? (lns->char lns row (fx1- col)))
             (loop row (fx1- col))]
           ; now we are past initial separators, find next separator
            [else
             (let loop ([col col])
               (cond
                 [(or (fx= col 0)
                      (separator? (lns->char lns row (fx1- col))))
                  (make-pos row col)]
                 [else (loop (fx1- col))]))])))))

  (module (indent indent-all)
    (define (calc-indent ee entry row)
      (define (find-unmatched-left-delim row)
        (let* ([ln (list-ref (entry-lns entry) row)]
               [s (ln-str ln)])
          (ln-str-set! ee ln (string rpchar))
          (let ([pos (find-matching-delim-backward ee entry row 0 #t)])
            (ln-str-set! ee ln s)
            pos)))
      (let ([lns (entry-lns entry)])
        (cond
          [(find-unmatched-left-delim row) =>
           (lambda (mpos)
             (let ([mrow (pos-row mpos)] [mcol (pos-col mpos)])
               (or
                  ; if some intervening line has same unmatched left
                  ; delimiter, use its indentation
                   (let f ([xrow (fx- row 1)])
                     (and (not (fx= xrow mrow))
                          (cond
                            [(find-unmatched-left-delim xrow) =>
                             (lambda (xmpos)
                               (if (pos=? xmpos mpos)
                                   (current-indent lns xrow)
                                   (f (fx- xrow 1))))]
                            [else (f (fx- xrow 1))])))
                  ; otherwise, if left paren is followed by a symbol,
                  ; indent under second item or use standard indent if
                  ; second item is too far out or not present
                   (let ([ip (open-input-string
                               (let ([s (lns->str lns mrow)])
                                 (substring s mcol (string-length s))))])
                     (on-error #f
                       (and (char=? (read-char ip) lpchar)
                         (let-values ([(t1 v1 s1 e1) (read-token ip)])
                           (and (and (eq? t1 'atomic) (symbol? v1))
                                (let-values ([(t2 v2 s2 e2) (read-token ip)])
                                  (if (and (not (eq? t2 'eof))
                                           (fx< s2 6)
                                          ; use standard indent for let and rec
                                           (not (memq v1 '(let rec))))
                                      (fx+ mcol s2)
                                      (fx+ mcol (ee-standard-indent)))))))))
                  ; otherwise, indent one space in.  this handles, among
                  ; other things, bracketed let bindings and cond clauses.
                   (fx+ mcol 1))))]
          [else 0])))

    (define current-indent
      (lambda (lns row)
        (let* ([s (lns->str lns row)]
               [n (string-length s)])
          (let f ([i 0])
            (if (and (fx< i n) (char=? (string-ref s i) #\space))
                (f (fx+ i 1))
                i)))))

    (define (indent-row! ee entry row n)
      (cond
        [(fx< n 0)
         (adjust-mark/delete ee entry row 0 row (fx- n))
         (let ([lns (entry-lns entry)])
           (lns->str! ee lns row
             (let ([s (lns->str lns row)])
               (substring s (fx- n) (string-length s)))))]
        [(fx> n 0)
         (adjust-mark/insert ee entry row 0 row n)
         (let ([lns (entry-lns entry)])
           (lns->str! ee lns row
             (string-append
               (make-string n #\space)
               (lns->str lns row))))]))

    (define (indent ee entry)
      (let* ([row (entry-row entry)]
             [lns (entry-lns entry)]
             [n (fx- (calc-indent ee entry row) (current-indent lns row))])
        (unless (fx= n 0)
          (let* ([ln (list-ref lns row)]
                 [nsr (ln-nsr ln)]
                 [eoe? (end-of-entry? ee entry)])
            (indent-row! ee entry row n)
            (move-bol ee entry)
            (let ([just-row? (fx= (ln-nsr ln) nsr)])
              (display-rest/goto ee entry just-row?
               ; avoid clear-eol/eos if inserting and either at end of entry or
               ; rewriting just the current row
                (or (fx< n 0) (and (not eoe?) (not just-row?)))
                row (fxmax (fx+ (entry-col entry) n) 0)))))))

    (define (indent-all ee entry)
      (let* ([lns (entry-lns entry)]
             [row (entry-row entry)]
             [col (entry-col entry)]
             [top-line (entry-top-line entry)]
             [point-ln (list-ref lns row)]
             [point-strlen (string-length (ln-str point-ln))]
             [lines-to-top ; compute before we muck with indentation
              (screen-lines-between ee entry
                (pos-row top-line) (pos-col top-line)
                row (col->line-offset ee col))])
        (let loop ([ls lns] [i 0] [firstmod (length lns)] [lastmod -1])
          (if (null? ls)
              (unless (and (fx< lastmod (pos-row top-line))
                           (fx> firstmod (pos-row (entry-bot-line entry))))
               ; move to first physical column of first displayed line
                (move-cursor-up lines-to-top)
                (carriage-return)
                (clear-eos)
                (entry-col-set! entry
                  (fxmax 0
                    (fx+ col
                         (fx- (string-length (ln-str point-ln))
                              point-strlen))))
                (redisplay ee entry))
              (let ([n (fx- (calc-indent ee entry i) (current-indent lns i))])
                (if (fx= n 0)
                    (loop (cdr ls) (fx+ i 1) firstmod lastmod)
                    (begin
                      (indent-row! ee entry i n)
                      (loop (cdr ls) (fx+ i 1)
                        (fxmin i firstmod)
                        (fxmax i lastmod)))))))))
  )

  (define (id-completions ee entry)
    (define (idstring<? prefix)
      (let ([common (ee-common-identifiers)]
            [scheme-syms (environment-symbols (scheme-environment))])
        (lambda (s1 s2)
          (let ([x1 (string->symbol (string-append prefix s1))]
                [x2 (string->symbol (string-append prefix s2))])
            ; prefer common
            (let ([m1 (memq x1 common)] [m2 (memq x2 common)])
              (if m1
                  (or (not m2) (< (length m2) (length m1)))
                  (and (not m2)
                       ; prefer user-defined
                       (let ([u1 (not (memq x1 scheme-syms))]
                             [u2 (not (memq x2 scheme-syms))])
                         (if u1
                             (or (not u2) (string<? s1 s2))
                             (and (not u2) (string<? s1 s2)))))))))))
    (define (completion str1 str2)
      (let ([n1 (string-length str1)] [n2 (string-length str2)])
        (and (fx>= n2 n1)
             (string=? (substring str2 0 n1) str1)
             (substring str2 n1 n2))))
    (define (fn-completions prefix)
      (values prefix
        (sort string<?
          (fold-left
            (let ([last (path-last prefix)])
              (lambda (suffix* s)
                (cond
                  [(completion last s) =>
                   (lambda (suffix) 
                     (cons (if (file-directory? (string-append prefix suffix))
                               (string-append suffix (string (directory-separator)))
                               suffix)
                       suffix*))]
                  [else suffix*])))
            '()
            (on-error '()
              (directory-list
                (let ([dir (path-parent prefix)])
                  (if (string=? dir "") "." dir))))))))
    (let loop ([c 0])
      (if (fx>= c (entry-col entry))
          (values #f '())
          (let ([s (let ([s (lns->str (entry-lns entry) (entry-row entry))])
                     (substring s c (string-length s)))])
            ((on-error
               (lambda ()
                 (if (and (fx> (string-length s) 0) (char=? (string-ref s 0) #\"))
                     (fn-completions (substring s 1 (string-length s)))
                     (loop (fx+ c 1))))
               (let-values ([(type value start end) (read-token (open-input-string s))])
                 (lambda ()
                   (cond
                     [(and (fx= (fx+ c end) (entry-col entry))
                           (eq? type 'atomic)
                           (symbol? value))
                      (let ([prefix (symbol->string value)])
                        (values prefix
                          (sort (idstring<? prefix)
                            (fold-left (lambda (suffix* x)
                                         (cond
                                           [(completion prefix (symbol->string x)) =>
                                            (lambda (suffix) (cons suffix suffix*))]
                                           [else suffix*]))
                              '() (environment-symbols (interaction-environment))))))]
                     [(and (fx= (fx+ c end -1) (entry-col entry))
                           (eq? type 'atomic)
                           (string? value))
                      (fn-completions value)]
                     [else (loop (fx+ c end))])))))))))

  (define (should-auto-indent? ee)
    (and (ee-auto-indent)
        ; don't autoindent if the characters are coming so fast that we're
        ; probably dealing with paste input
         (> (- (real-time) (car (eestate-rt-last-op ee))) 50)))
)

(module (ee-read)
  (define (accept ee entry kf)
    (let* ([str (entry->string entry)]
           [sip (open-input-string str)])
      (define (fail c)
        (define (report sop)
          (cond
            [(and (message-condition? c)
                  (irritants-condition? c)
                  (equal? (condition-message c) "~? at char ~a of ~s")
                  (let ([irritants (condition-irritants c)])
                    (and (list? irritants)
                         (fx= (length irritants) 4)
                         irritants))) =>
             (lambda (irritants)
               (apply
                 (lambda (?msg ?args fp ip)
                   (fprintf sop "read: ~?" ?msg ?args)
                   (let ([pos (index->pos str fp 0 0)])
                     (entry-row-set! entry (pos-row pos))
                     (entry-col-set! entry (pos-col pos))))
                 irritants))]
            [else (display-condition c sop)]))
       ; clear entry before report has a chance to muck with point position
        (clear-entry ee entry)
        (ee-display-string (make-string (screen-cols) #\-))
        (carriage-return)
        (line-feed)
        (let* ([s (let ([sop (open-output-string)])
                    (report sop)
                    (get-output-string sop))]
               [n (string-length s)])
          (let loop ([i 0] [msg-lines 0])
            (if (= i n)
                (begin
                  (unless (fx< (screen-rows) 3)
                    (ee-display-string (make-string (screen-cols) #\-))
                    (carriage-return)
                    (line-feed))
                  (redisplay ee entry (max (fx- (screen-rows) msg-lines 2) 1)))
                (let ([m (min (fx+ i (screen-cols)) n)])
                  (ee-display-string (substring s i m))
                  (when (fx< (screen-rows) 2) (wait 2000))
                  (carriage-return)
                  (line-feed)
                  (loop m (fx+ msg-lines 1))))))
        (kf))
      (define (succeed result)
        (move-eoe ee entry)
        (no-raw-mode)
        (ee-write-char #\newline)
        (ee-flush)
        (update-history! ee entry)
       ; skip close delimiters, whitespace, and comments, then
       ; save remainder of entry, if any, as histnow
        (eestate-histnow-set! ee
          (substring str
            (let skip ([fp (file-position sip)])
              (on-error fp
                (let-values ([(type value start end) (read-token sip)])
                  (case type
                    [(rparen rbrack) (skip end)]
                    [else start]))))
            (string-length str)))
        (eestate-last-op-set! ee #f)
       ; inform encapsulated transcript port(s) if any
        (let loop ([op (console-output-port)])
          (when ($xscript-port? op)
            (let-values ([(ip op xp) ($constituent-ports op)])
              (unless (port-closed? xp) (echo-entry ee entry xp))
              (loop op))))
        result)
      ((guard (c [#t (lambda () (fail c))])
         (let ([x (read sip)])
           (lambda () (succeed x)))))))

  (define (dispatch ee entry table)
    (if (ee-winch?)
        (begin
          (handle-winch ee entry)
          (dispatch ee entry table))
        (let ([c (ee-read-char)])
          (let ([x (if (eof-object? c)
                       (lambda (ee entry c) #f)
                       (hashtable-ref table c ee-insert-self))])
            (cond
              [(procedure? x)
               (let ([n (eestate-repeat-count ee)])
                 (eestate-repeat-count-set! ee 1)
                 (if (= n 0)
                     (dispatch ee entry base-dispatch-table)
                     (let loop ([n n] [entry entry])
                       (cond
                         [(x ee entry c) =>
                          (lambda (entry)
                            (if (> n 1)
                                (loop (- n 1) entry)
                                (begin
                                  (eestate-rt-last-op-set! ee
                                    (cons (cdr (eestate-rt-last-op ee))
                                          (real-time)))
                                  (eestate-last-op-set! ee x)
                                  (dispatch ee entry base-dispatch-table))))]
                         [else
                          (accept ee entry
                            (lambda ()
                              (dispatch ee entry base-dispatch-table)))]))))]
              [(dispatch-table? x) (dispatch ee entry x)]
              [else
               (eestate-repeat-count-set! ee 1)
               (eestate-last-op-set! ee #f)
               (beep "unbound key")
               (dispatch ee entry base-dispatch-table)])))))

  (define (ee-read ee)
    (screen-resize!)
    (let ([entry (let ([s (eestate-histnow ee)])
                  ; set to "" so that entry will appear modified if nonempty,
                  ; i.e., if a partial entry is left over from last read
                   (eestate-histnow-set! ee "")
                   (string->entry ee s))])
      (raw-mode)
      (carriage-return)
      (redisplay ee entry)
      (move-eol ee entry)
      (guard (c [#t (carriage-return)
                    (line-feed)
                    (clear-eos)
                    (ee-flush)
                    (no-raw-mode)
                    (ee-display-string
                      (call-with-string-output-port
                        (lambda (p)
                          (display-condition c p))))
                    (ee-write-char #\newline)
                    (update-history! ee entry)
                    (void)])
        (dispatch ee entry base-dispatch-table)))))

(define (ee-prompt-and-read ee n)
  (unless (and (integer? n) (>= n 0))
    ($oops 'ee-prompt-and-read
      "nesting level ~s is not a positive integer"
      n))
  (if (and (let f ([ip (console-input-port)])
             (or (eq? ip #%$console-input-port)
                 (and ($xscript-port? ip)
                   (let-values ([(ip op xp) ($constituent-ports ip)])
                     (f ip)))))
           (let f ([op (console-output-port)])
             (or (eq? op #%$console-output-port)
                 (and ($xscript-port? op)
                   (let-values ([(ip op xp) ($constituent-ports op)])
                     (f op))))))
      (begin
       ; fresh-line doesn't take into account output written to the console
       ; through some other port or external means, so this might not emit a
       ; fresh line when one is needed, but the user can always redisplay
        (fresh-line (console-output-port))
        (flush-output-port (console-output-port))
        (eestate-prompt-set! ee
          (let ([wps (waiter-prompt-string)])
            (if (string=? wps "")
                ""
                (string-append
                  (apply string-append (make-list n wps))
                  " "))))
        (ee-read ee))
      (default-prompt-and-read n)))

;;; history functions

(module (history-search-bwd history-search-fwd
         update-history! history-fast-forward! entry-modified?
         ee-save-history ee-load-history)
  (define search
    (lambda (ee pred? get-bwd set-bwd! get-fwd set-fwd!)
      (let loop ([bwd (get-bwd ee)]
                 [now (eestate-histnow ee)]
                 [fwd (get-fwd ee)])
        (and (not (null? bwd))
             (let ([s (car bwd)])
               (if (pred? s)
                   (begin
                     (set-bwd! ee (cdr bwd))
                     (eestate-histnow-set! ee s)
                     (set-fwd! ee (cons now fwd))
                     s)
                   (loop (cdr bwd) s (cons now fwd))))))))

  (define history-search-bwd
    (lambda (ee pred?)
      (search ee pred? eestate-histbwd eestate-histbwd-set!
        eestate-histfwd eestate-histfwd-set!)))

  (define history-search-fwd
    (lambda (ee pred?)
      (search ee pred? eestate-histfwd eestate-histfwd-set!
        eestate-histbwd eestate-histbwd-set!)))

  (define history->list
    (lambda (ee)
      (cdr `(,@(reverse (eestate-histfwd ee))
             ,(eestate-histnow ee)
             ,@(eestate-histbwd ee)))))

  (define trim-history
    (lambda (ls)
      (let ([n (ee-history-limit)])
        (if (> (length ls) n)
            (list-head ls n)
            ls))))

  (define update-history!
    (lambda (ee entry)
      (define (all-whitespace? s)
        (let ([n (string-length s)])
          (let f ([i 0])
            (or (fx= i n)
                (and (memv (string-ref s i) '(#\space #\newline))
                     (f (fx+ i 1)))))))
      (let ([s (entry->string entry)] [ls (history->list ee)])
        (eestate-histbwd-set! ee
          (if (or (all-whitespace? s)
                  (and (not (null? ls))
                       (equal? s (car ls))))
              ls
              (begin
                (eestate-histnew-set! ee (fx+ (eestate-histnew ee) 1))
                (trim-history (cons s ls))))))
      (eestate-histnow-set! ee "")
      (eestate-histfwd-set! ee '())))

  (define history-fast-forward!
    (lambda (ee)
      (eestate-histbwd-set! ee (history->list ee))
      (eestate-histnow-set! ee "")
      (eestate-histfwd-set! ee '())))

  (define (entry-modified? ee entry)
    (not (string=? (entry->string entry) (eestate-histnow ee))))

  (module (ee-save-history ee-load-history)
    (define read-history
      (lambda (ip)
        (on-error #f
          (let loop ([ls '()])
            (let ([x (read ip)])
              (if (eof-object? x)
                  ls
                  (begin
                    (unless (string? x) ($oops #f "oops"))
                    (loop (cons x ls)))))))))

    (define ee-save-history
      (lambda (ee filename)
        (unless (string? filename)
          ($oops 'ee-save "~s is not a string" filename))
        (let* ([iop ($open-file-input/output-port 'expeditor filename
                      (file-options exclusive no-fail no-truncate)
                      (buffer-mode block)
                      (make-transcoder (utf-8-codec)))]
               [ls (let ([curls (history->list ee)])
                     (cond
                       [(read-history iop) =>
                        (lambda (savls)
                          (trim-history
                            (append
                              (list-head curls (eestate-histnew ee))
                              savls)))]
                       [else curls]))])
          (truncate-file iop)
          (fprintf iop "~
            ;;; This file contains a saved history for the (Petite) Chez Scheme~@
            ;;; expression editor.  The history is represented as a sequence of~@
            ;;; strings, each representing a history entry, with the most recent~@
            ;;; entries listed last.~@
            ~@
            ;;; Exit each Scheme session running the expression editor before~@
            ;;; saving changes so they aren't wiped out when the session ends.\n\n")
          (for-each (lambda (s) (fprintf iop "~s\n" s)) (reverse ls))
          (close-port iop))))

    (define ee-load-history
      (lambda (ee filename)
        (unless (string? filename)
          ($oops 'ee-load-history "~s is not a string" filename))
        (let* ([iop ($open-file-input/output-port 'expeditor filename
                      (file-options exclusive no-fail no-truncate)
                      (buffer-mode block)
                      (make-transcoder (utf-8-codec)))]
               [ls (read-history iop)])
          (close-port iop)
          (unless ls
            ($oops 'ee-load-history "missing or malformed history file ~s"
              filename))
          (eestate-histnew-set! ee 0)
          (eestate-histbwd-set! ee ls)
          (eestate-histnow-set! ee "")
          (eestate-histfwd-set! ee '())))))
)

;;; editing functions

(module (ee-next-id-completion ee-next-id-completion/indent)
  (define complete
    (lambda (ee entry suffix*)
      (eestate-last-suffix*-set! ee suffix*)
      (if (null? suffix*)
          (beep "id-completion: no completion found")
          (insert-string-before ee entry (car suffix*)))))

  (define next-completion
    (lambda (ee entry)
      (if (fx<= (length (eestate-last-suffix* ee)) 1)
          (beep "id-completion: no completion found")
          (let ([suffix (car (eestate-last-suffix* ee))])
            (let ([n (string-length suffix)])
              (move-left ee entry n)
              (delete-forward ee entry (entry-row entry) (fx+ (entry-col entry) n)))
            (complete ee entry
              (append (cdr (eestate-last-suffix* ee)) (list suffix)))))))

  (define ee-next-id-completion
    (lambda (ee entry c)
      (if (eq? (eestate-last-op ee) ee-next-id-completion)
          (next-completion ee entry)
          (let-values ([(prefix suffix*) (id-completions ee entry)])
            (if prefix
                (complete ee entry suffix*)
                (begin
                  (eestate-last-suffix*-set! ee '())
                  (beep "id-completion: no identifier to complete")))))
      entry))

  (define ee-next-id-completion/indent
    (lambda (ee entry c)
      (cond
        [(and (eq? (eestate-last-op ee) ee-next-id-completion/indent)
              (eestate-cc? ee))
         (next-completion ee entry)
         entry]
        [(and (or (eq? (eestate-last-op ee) ee-insert-self)
                  (eq? (eestate-last-op ee) ee-next-id-completion/indent))
              (let-values ([(prefix suffix*) (id-completions ee entry)])
                (and prefix suffix*))) =>
         (lambda (suffix*)
           (eestate-cc?-set! ee #t)
           (complete ee entry suffix*)
           entry)]
        [else
         (eestate-cc?-set! ee #f)
         (eestate-last-suffix*-set! ee '())
         (ee-indent ee entry c)])))
)
  
(module (ee-id-completion ee-id-completion/indent)
  (define (display-completions prefix suffix*)
    (let* ([s* (map (lambda (suffix) (string-append prefix suffix)) suffix*)]
           [width (fx+ (apply fxmax (map string-length s*)) 2)]
           [tcols (fxmax 1 (fxquotient (screen-cols) width))]
           [trows (fxquotient (length s*) tcols)]
           [nlong (fxremainder (length s*) tcols)])
      (define (display-row v last)
        (let loop ([j 0])
          (let ([s (vector-ref v j)])
            (if (fx= j last)
                (ee-display-string s)
                (begin
                  (ee-display-string (format "~va" width s))
                  (loop (fx+ j 1))))))
        (carriage-return)
        (line-feed))
      (let ([v (make-vector (if (fx= nlong 0) trows (fx+ trows 1)))])
        (do ([i 0 (fx+ i 1)])
            ((fx= i (vector-length v)))
          (vector-set! v i (make-vector tcols #f)))
        (let f ([s* s*] [i 0] [j 0] [nlong nlong])
          (unless (null? s*)
            (if (fx= i (if (fx> nlong 0) (fx+ trows 1) trows))
                (f s* 0 (fx+ j 1) (fx- nlong 1))
                (begin
                  (vector-set! (vector-ref v i) j (car s*))
                  (f (cdr s*) (fx+ i 1) j nlong)))))
        (do ([i 0 (fx+ i 1)])
            ((fx= i trows))
          (display-row (vector-ref v i) (fx- tcols 1)))
        (unless (fx= nlong 0)
          (display-row (vector-ref v trows) (fx- nlong 1)))
        (if (fx= nlong 0) trows (fx+ trows 1)))))

  (define (common-prefix s*)
    (let outer ([s1 (car s*)] [s* (cdr s*)])
      (if (null? s*)
          s1
          (let ([s2 (car s*)])
            (let ([n1 (string-length s1)] [n2 (string-length s2)])
              (let inner ([i 0])
                (if (or (fx= i n1)
                        (fx= i n2)
                        (not (char=? (string-ref s1 i) (string-ref s2 i))))
                    (outer (substring s1 0 i) (cdr s*))
                    (inner (fx+ i 1)))))))))

  (define ee-id-completion
    (lambda (ee entry c)
      (let-values ([(prefix suffix*) (id-completions ee entry)])
        (if prefix
            (if (not (null? suffix*))
                (if (eq? (eestate-last-op ee) ee-id-completion)
                    (begin
                      (clear-entry ee entry)
                      (ee-display-string (make-string (screen-cols) #\-))
                      (carriage-return)
                      (line-feed)
                      (let ([nrows (display-completions prefix suffix*)])
                        (ee-display-string (make-string (screen-cols) #\-))
                        (carriage-return)
                        (line-feed)
                        (redisplay ee entry (max (fx- (screen-rows) nrows 1) 1))))
                    (insert-string-before ee entry (common-prefix suffix*)))
                (beep "id-completion: no completions found"))
            (beep "id-completion: no identifier to complete")))
      entry))

  (define ee-id-completion/indent
    (lambda (ee entry c)
      (cond
        [(and (eq? (eestate-last-op ee) ee-id-completion/indent)
              (eestate-cc? ee))
         (let-values ([(prefix suffix*) (id-completions ee entry)])
           (if (not (null? suffix*))
               (begin
                 (clear-entry ee entry)
                 (ee-display-string (make-string (screen-cols) #\-))
                 (carriage-return)
                 (line-feed)
                 (let ([nrows (display-completions prefix suffix*)])
                   (ee-display-string (make-string (screen-cols) #\-))
                   (carriage-return)
                   (line-feed)
                   (redisplay ee entry (max (fx- (screen-rows) nrows 1) 1))))
               (beep "id-completion: no completions found")))
         entry]
        [(and (or (eq? (eestate-last-op ee) ee-insert-self)
                  (eq? (eestate-last-op ee) ee-id-completion/indent))
              (let-values ([(prefix suffix*) (id-completions ee entry)])
                (and prefix suffix*))) =>
         (lambda (suffix*)
           (eestate-cc?-set! ee #t)
           (if (not (null? suffix*))
               (insert-string-before ee entry (common-prefix suffix*))
               (beep "id-completion: no completions found"))
           entry)]
        [else
         (eestate-cc?-set! ee #f)
         (ee-indent ee entry c)])))
)

(define ee-insert-self
  (lambda (ee entry c)
    (add-char ee entry c)
    entry))

(define ee-command-repeat
  (lambda (ee entry c)
    (define (digit-value c) (char- c #\0))
    (eestate-repeat-count-set! ee
      (let ([c (ee-peek-char)])
        (if (and (not (eof-object? c)) (char-numeric? c))
            (let loop ([n (digit-value (ee-read-char))])
              (let ([c (ee-peek-char)])
                (if (and (not (eof-object? c)) (char-numeric? c))
                    (loop (+ (* n 10) (digit-value (ee-read-char))))
                    n)))
            (ee-default-repeat))))
    entry))

(module (ee-history-bwd ee-history-fwd
         ee-history-bwd-prefix ee-history-fwd-prefix
         ee-history-bwd-contains ee-history-fwd-contains)
  (define contains?
    (lambda (key str)
      (let ([key-len (string-length key)]
            [str-len (string-length str)])
        (let loop ([idx 0])
          (cond
            [(fx> key-len (fx- str-len idx)) #f]
            [(string=? key (substring str idx (fx+ idx key-len))) #t]
            [else (loop (add1 idx))])))))

  (define prefix?
    (lambda (key str)
      (let ([nkey (string-length key)] [nstr (string-length str)])
       ; if key doesn't start with space, skip leading spaces in str
        (let ([i (if (or (fx= nkey 0) (char=? (string-ref key 0) #\space))
                     0
                     (let f ([i 0])
                       (if (or (fx= i nstr) (not (char=? (string-ref str i) #\space)))
                           i
                           (f (fx+ i 1)))))])
          (let ([n (fx+ nkey i)])
            (and (fx<= n nstr)
                 (string=? key (substring str i n))))))))

  (define new-entry
    (lambda (ee entry s)
      (clear-entry ee entry)
      (let ([entry (string->entry ee s)])
        (redisplay ee entry 1)
        (move-eol ee entry)
        entry)))

  (define ee-history-bwd
    (lambda (ee entry c)
      (cond
        [(and (not (null-entry? entry)) (entry-modified? ee entry))
         (beep "cannot leave nonempty modified entry")
         entry]
        [(history-search-bwd ee (lambda (s) #t)) =>
         (lambda (s)
          ; clear histkey when null as favor to search commands
           (when (null-entry? entry) (eestate-histkey-set! ee ""))
           (new-entry ee entry s))]
        [else
         (beep "invalid history movement")
         entry])))

  (define ee-history-fwd
    (lambda (ee entry c)
      (cond
        [(and (not (null-entry? entry)) (entry-modified? ee entry))
         (beep "cannot leave nonempty modified entry")
         entry]
        [(history-search-fwd ee (lambda (s) #t)) =>
         (lambda (s)
          ; clear histkey when null as favor to search commands
           (when (null-entry? entry) (eestate-histkey-set! ee ""))
           (new-entry ee entry s))]
        [else
         (beep "invalid history movement")
         entry])))

  (define history-search-bwd-key
    (lambda (ee entry match?)
      (if (or (entry-modified? ee entry) (null-entry? entry))
          (begin
            (history-fast-forward! ee)
            (eestate-histkey-set! ee (entry->string entry))
            (cond
              [(history-search-bwd ee
                 (lambda (s) (match? (eestate-histkey ee) s))) =>
               (lambda (s) (new-entry ee entry s))]
              [else
               (beep "invalid history movement")
               entry]))
         ; if nonempty and unmodified, we must already have moved via one
         ; of the history commands, so eestate-histkey should be valid
          (cond
            [(history-search-bwd ee
               (lambda (s) (match? (eestate-histkey ee) s))) =>
             (lambda (s) (new-entry ee entry s))]
            [else
             (beep "invalid history movement")
             entry]))))

  (define history-search-fwd-key
   ; similar to history-search-bwd-key but "finds" key at forward extreme
    (lambda (ee entry match?)
      (if (or (entry-modified? ee entry) (null-entry? entry))
          (begin
            (history-fast-forward! ee)
            (eestate-histkey-set! ee (entry->string entry))
            (cond
              [(history-search-fwd ee
                 (lambda (s) (prefix? (eestate-histkey ee) s))) =>
               (lambda (s) (new-entry ee entry s))]
              [else
               (beep "invalid history movement")
               entry]))
         ; if nonempty and unmodified, we must already have moved via one
         ; of the history commands, so eestate-histkey should be valid
          (cond
            [(history-search-fwd ee
               (lambda (s) (match? (eestate-histkey ee) s))) =>
             (lambda (s) (new-entry ee entry s))]
            [else
             (let ([entry (new-entry ee entry (eestate-histkey ee))])
               (history-fast-forward! ee)
               entry)]))))

  (define ee-history-fwd-prefix
    (lambda (ee entry c)
      (history-search-fwd-key ee entry prefix?)))

  (define ee-history-bwd-prefix
    (lambda (ee entry c)
      (history-search-bwd-key ee entry prefix?)))

  (define ee-history-fwd-contains
    (lambda (ee entry c)
      (history-search-fwd-key ee entry contains?)))

  (define ee-history-bwd-contains
    (lambda (ee entry c)
      (history-search-bwd-key ee entry contains?)))
)

(define ee-newline/accept
  (lambda (ee entry c)
    (cond
      [(null-entry? entry) entry]
     ; #f tells ee-read to return expr
      [(and (find-next-sexp-forward ee entry 0 0 #t)
            (only-whitespace-left? ee entry))
       (let loop ()
         (delete-to-eol ee entry)
         (unless (last-line? ee entry)
           (join-rows ee entry)
           (loop)))
       #f]
      [else
       (insert-strings-before ee entry '("" ""))
       (when (should-auto-indent? ee) (indent ee entry))
       entry])))

(define ee-newline
  (lambda (ee entry c)
    (cond
      [(null-entry? entry) entry]
      [else
       (insert-strings-before ee entry '("" ""))
       (when (should-auto-indent? ee) (indent ee entry))
       entry])))

(define ee-accept
  (lambda (ee entry c)
   ; force ee-read to attempt read even if not at end of expr and not balanced
    (on-error #f
      (let ([sip (open-input-string (entry->string entry))])
        (let loop ()
          (let-values ([(type value start end) (read-token sip)])
            (cond
              [(eq? type 'eof)
              ; entry contains only whitespace and comments.  pretend to accept
              ; but don't really, or ee-read will return eof, causing cafe to exit
               (update-history! ee entry)
               (move-eoe ee entry)
               (no-raw-mode)
               (ee-write-char #\newline)
               (ee-flush)
               (raw-mode)
               (let ([entry (string->entry ee "")])
                 (redisplay ee entry)
                 entry)]
              [(and (eq? type 'quote) (eq? value 'datum-comment))
               (read sip)
               (loop)]
              [else #f])))))))

(define ee-open-line
  (lambda (ee entry c)
    (let ([point (entry-point entry)])
      (insert-strings-before ee entry '("" ""))
      (when (should-auto-indent? ee) (indent ee entry))
      (goto ee entry point)
      entry)))

(define ee-indent
  (lambda (ee entry c)
    (indent ee entry)
    entry))

(define ee-indent-all
  (lambda (ee entry c)
    (indent-all ee entry)
    entry))

(define ee-backward-char
  (lambda (ee entry c)
    (if (beginning-of-line? ee entry)
        (unless (first-line? ee entry)
          (move-up ee entry)
          (move-eol ee entry))
        (move-left ee entry))
    entry))

(define ee-forward-char
  (lambda (ee entry c)
    (if (end-of-line? ee entry)
        (unless (last-line? ee entry)
          (move-down ee entry)
          (move-bol ee entry))
        (move-right ee entry))
    entry))

(define ee-next-line
  (lambda (ee entry c)
    (if (last-line? ee entry)
        (ee-history-fwd ee entry c)
        (begin
          (move-down ee entry)
          entry))))

(define ee-previous-line
  (lambda (ee entry c)
    (if (first-line? ee entry)
        (ee-history-bwd ee entry c)
        (begin
          (move-up ee entry)
          entry))))

(define ee-end-of-line
  (lambda (ee entry c)
    (move-eol ee entry)
    entry))

(define ee-beginning-of-line
  (lambda (ee entry c)
    (move-bol ee entry)
    entry))

(define ee-beginning-of-entry
  (lambda (ee entry c)
    (goto ee entry (make-pos 0 0))
    entry))

(define ee-end-of-entry
  (lambda (ee entry c)
    (move-eoe ee entry)
    entry))

(define ee-delete-to-eol
  (lambda (ee entry c)
    (if (end-of-line? ee entry)
        (unless (last-line? ee entry)
          (join-rows ee entry)
          (eestate-killbuf-set! ee
            (if (eq? (eestate-last-op ee) ee-delete-to-eol)
                (append (eestate-killbuf ee) '(""))
                '(""))))
        (eestate-killbuf-set! ee
          (let ([killbuf (delete-to-eol ee entry)])
            (if (eq? (eestate-last-op ee) ee-delete-to-eol)
               ; last addition must have been ("") representing newline
                (append (reverse (cdr (reverse (eestate-killbuf ee))))
                        killbuf)
                killbuf))))
    entry))

(define ee-delete-line
  (lambda (ee entry c)
    (if (and (first-line? ee entry)
             (not (last-line? ee entry))
             (last-line-displayed? ee entry))
        (ee-delete-entry ee entry c)
        (begin
          (move-bol ee entry)
          (let ([killbuf (delete-to-eol ee entry)])
             (unless (equal? killbuf '(""))
               (eestate-killbuf-set! ee killbuf)))
          entry))))

(define ee-delete-between-point-and-mark
  (lambda (ee entry c)
    (let ([point (entry-point entry)] [mark (entry-mark entry)])
      (if mark
          (unless (pos=? mark point)
            (eestate-killbuf-set! ee
              (if (pos<? mark (entry-point entry))
                  (begin
                    (goto ee entry mark)
                    (delete-forward ee entry (pos-row point) (pos-col point)))
                  (delete-forward ee entry (pos-row mark) (pos-col mark)))))
          (beep "mark not set")))
    entry))

(define ee-set-mark
  (lambda (ee entry c)
    (entry-mark-set! entry (entry-point entry))
    entry))

(define ee-delete-entry
  (lambda (ee entry c)
    (unless (null-entry? entry)
      (eestate-killbuf-set! ee (yank-entry ee entry)))
    (clear-entry ee entry)
    (let ([entry (string->entry ee "")])
      (redisplay ee entry)
      entry)))

(define ee-reset-entry
  (lambda (ee entry c)
    (history-fast-forward! ee)
    (ee-delete-entry ee entry c)))

(define ee-delete-sexp
  (lambda (ee entry c)
    (let ([pos (find-next-sexp-forward ee entry
                 (entry-row entry) (entry-col entry) #f)])
      (if pos
          (eestate-killbuf-set! ee
            (delete-forward ee entry (pos-row pos) (pos-col pos)))
          (beep "end of s-expression not found")))
    entry))

(define ee-backward-delete-sexp
  (lambda (ee entry c)
    (let ([row (entry-row entry)] [col (entry-col entry)])
      (let ([pos (find-next-sexp-backward ee entry row col)])
        (if pos
            (begin
              (goto ee entry pos)
              (eestate-killbuf-set! ee (delete-forward ee entry row col)))
          (beep "start of s-expression not found"))))
    entry))

(define ee-redisplay
  (lambda (ee entry c)
    (if (eq? (eestate-last-op ee) ee-redisplay)
        (clear-screen)
        (clear-entry ee entry))
    (redisplay ee entry)
    entry))

(define ee-yank-kill-buffer
  (lambda (ee entry c)
    (insert-strings-before ee entry (eestate-killbuf ee))
    entry))

(define ee-yank-selection
  (lambda (ee entry c)
    (insert-strings-before ee entry
      (string->lines
        (let* ([s (get-clipboard)]
               [n (fx- (string-length s) 1)])
          (if (and (fx>= n 0) (char=? (string-ref s n) #\newline))
              (substring s 0 n)
              s))))
    entry))

(define ee-string-macro
  (lambda (str)
    (lambda (ee entry c)
      (insert-string-before ee entry str)
      entry)))

(define ee-eof
  (lambda (ee entry c)
    (cond
      [(null-entry? entry) #f]
      [else (beep "eof ignored except in null entry")])))

(define ee-delete-char
  (lambda (ee entry c)
    (cond
      [(end-of-line? ee entry)
       (unless (last-line? ee entry) (join-rows ee entry))
       entry]
      [else (delete-char ee entry) entry])))

(define ee-eof/delete-char
  (lambda (ee entry c)
    (cond
      [(null-entry? entry)
       (if (eq? (eestate-last-op ee) ee-eof/delete-char)
           entry     ; assume attempt to continue deleting chars
           #f)]
      [(end-of-line? ee entry)
       (unless (last-line? ee entry) (join-rows ee entry))
       entry]
      [else (delete-char ee entry) entry])))

(define ee-backward-delete-char
  (lambda (ee entry c)
    (if (beginning-of-line? ee entry)
        (unless (first-line? ee entry)
          (move-up ee entry)
          (move-eol ee entry)
          (join-rows ee entry))
        (begin
          (move-left ee entry)
          (delete-char ee entry)))
    entry))

(define ee-insert-paren
  (lambda (ee entry c)
    (add-char ee entry c)
    (when (or (ee-flash-parens) (ee-auto-paren-balance))
      (correct&flash-matching-delimiter ee entry))
    entry))

(define ee-goto-matching-delimiter
  (lambda (ee entry c)
    (let ([pos (find-matching-delimiter ee entry)])
      (if pos
          (goto ee entry pos)
          (beep "matching delimiter not found")))
    entry))

(define ee-flash-matching-delimiter
  (lambda (ee entry c)
    (let ([pos (find-matching-delimiter ee entry)])
      (if pos
          (flash ee entry pos)
          (beep "matching delimiter not found")))
    entry))

(define ee-exchange-point-and-mark
  (lambda (ee entry c)
    (let ([mark (entry-mark entry)])
      (if mark
          (begin
            (entry-mark-set! entry (entry-point entry))
            (goto ee entry mark))
          (beep "mark not set")))
    entry))

(define ee-forward-sexp
  (lambda (ee entry c)
    (let ([pos (find-next-sexp-forward ee entry
                 (entry-row entry) (entry-col entry) #f)])
      (if pos
          (goto ee entry pos)
          (beep "end of s-expression not found")))
    entry))

(define ee-backward-sexp
  (lambda (ee entry c)
    (let ([pos (find-next-sexp-backward ee entry
                 (entry-row entry) (entry-col entry))])
      (if pos
          (goto ee entry pos)
          (beep "start of s-expression not found")))
    entry))

(define ee-forward-word
  (lambda (ee entry c)
    (goto ee entry
      (find-next-word ee entry
        (entry-row entry)
        (entry-col entry)))
    entry))

(define ee-backward-word
  (lambda (ee entry c)
    (goto ee entry
      (find-previous-word ee entry
        (entry-row entry)
        (entry-col entry)))
    entry))

(define ee-forward-page
  (lambda (ee entry c)
    (page-down ee entry)
    entry))

(define ee-backward-page
  (lambda (ee entry c)
    (page-up ee entry)
    entry))

(define ee-suspend-process
  (lambda (ee entry c)
    (carriage-return)
    (line-feed)
    (clear-eos)
    (ee-flush)
    (no-raw-mode)
    (pause)
    (raw-mode)
    (carriage-return)
    (clear-eos)
    (redisplay ee entry)
    entry))

(define (ee-compose . p*)
  (rec ee-composition
    (lambda (ee entry c)
      (let f ([p* p*] [entry entry])
        (if (null? p*)
            entry
            (let ([entry ((car p*) ee entry c)])
              (and entry (f (cdr p*) entry))))))))

;;; key bindings

;;; (ee-bind-key key ee-xxx)

;;; key must evaluate to a <key>, where:
;;;
;;; <key> = <char> | <key-string>
;;;
;;; <key-string> -> "<key-char>+"
;;; <key-char> ->
;;;   \e             escape character
;;;   ^x             control is applied to character x
;;;   \\             backslash
;;;   \^             caret
;;;   <plain-char>   any character other than \ or ^
;;;
;;; <char> examples:
;;;
;;;   input key   description  byte sequence
;;;   ---------   -----------  -------------
;;;   #\a         letter 'a'   97
;;;   #\^         caret        94
;;;
;;; <key-string> examples:
;;;
;;;   input key   contents  description  byte sequence
;;;   ---------   --------  -----------  -------------
;;;   "\\ex"      \ex       Esc-x        27 120
;;;   "^a"        ^a        Ctrl-A       1
;;;   "\\\\"      \\        backslash    92
;;;   "\\^"       \^        caret        94
;;;   "a"         a         letter 'a'   97

(module (dispatch-table? base-dispatch-table ee-bind-key)
  (define make-dispatch-table
    (lambda ()
      (make-eqv-hashtable 256)))

  (define dispatch-table? hashtable?)

  (define ee-bind-key
    (lambda (key proc)
      (unless (or (char? key)
                  (and (string? key) (fx> (string-length key) 0)))
        ($oops 'ee-bind-key "~s is not a valid key (character or nonempty string)" key))
      (unless (procedure? proc)
        ($oops 'ee-bind-key "~s is not a procedure" proc))

      (if (string? key)
          (let* ([n (string-length key)])
            (define (s0 table i)
              (let ([c (string-ref key i)])
                (case c
                  [(#\\) (s-backslash table (fx+ i 1))]
                  [(#\^) (s-caret table (fx+ i 1))]
                  [else (s-lookup table (fx+ i 1) c)])))
            (define (s-backslash table i)
              (when (fx= i n)
                ($oops 'ee-bind-key
                  "malformed key ~s (nothing following \\)"
                  key))
              (let ([c (string-ref key i)])
                (case c
                  [(#\e) (s-lookup table (fx+ i 1) #\esc)]
                  [(#\\ #\^) (s-lookup table (fx+ i 1) c)]
                  [else ($oops 'ee-bind-key
                         "malformed key ~s (unexpected character following \\)"
                         key)])))
            (define (s-caret table i)
              (define (^char c)
                (integer->char (fxlogand (char->integer c) #b11111)))
              (when (fx= i n)
                ($oops 'ee-bind-key
                  "malformed key ~s (nothing following ^)"
                  key))
              (s-lookup table (fx+ i 1) (^char (string-ref key i))))
            (define (s-lookup table i key)
              (let ([x (hashtable-ref table key #f)])
                (cond
                  [(fx= i n)
                   (when (dispatch-table? x)
                     (warningf 'ee-bind-key
                       "definition for key ~s disables its use as a prefix"
                       key))
                   (hashtable-set! table key proc)]
                  [(dispatch-table? x) (s0 x i)]
                  [else
                   (when (procedure? x)
                     (warningf 'ee-bind-key
                       "definition for key ~s disables its use as a prefix"
                       key))
                   (let ([x (make-dispatch-table)])
                     (hashtable-set! table key x)
                     (s0 x i))])))
            (s0 base-dispatch-table 0))
          (begin
            (when (dispatch-table? (hashtable-ref base-dispatch-table key #f))
              (warningf 'ee-bind-key
                "definition for key ~s disables its use as a prefix"
                key))
            (hashtable-set! base-dispatch-table key proc)))))

  (define base-dispatch-table (make-dispatch-table))

 ; set up self-insertion for space and all printing characters
  (for-each
    (lambda (c) (ee-bind-key c ee-insert-self))
    (string->list " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"))
)

(let ([ebk ee-bind-key])
 ; newline operations
  (ebk #\return   ee-newline/accept)                  ; Enter, ^M
  (ebk "^J"       ee-accept)                          ; ^J
  (ebk "^O"       ee-open-line)                       ; ^O

 ; indenting operations
  (ebk "\\e\t"    ee-indent)                          ; Esc-Tab
  (ebk "\\eq"     ee-indent-all)                      ; Esc-q
  (ebk "\\eQ"     ee-indent-all)                      ; Esc-Q
  (ebk "\\e^Q"    ee-indent-all)                      ; Esc-^Q

 ; command completion
  (ebk "\t"       ee-id-completion/indent)            ; Tab
  (ebk "^R"       ee-next-id-completion)              ; ^R

 ; cursor movement keys
  (ebk "^B"       ee-backward-char)                   ; ^B
  (ebk "\\e[D"    ee-backward-char)                   ; Left       ; ]
  (ebk "^F"       ee-forward-char)                    ; ^F
  (ebk "\\e[C"    ee-forward-char)                    ; Right      ; ]
  (ebk "^N"       ee-next-line)                       ; ^N
  (ebk "\\e[B"    ee-next-line)                       ; Down
  (ebk "^P"       ee-previous-line)                   ; ^P
  (ebk "\\e[A"    ee-previous-line)                   ; Up

  (ebk "\\ef"     ee-forward-word)                    ; Esc-f
  (ebk "\\eF"     ee-forward-word)                    ; Esc-F
  (ebk "\\e^F"    ee-forward-sexp)                    ; Esc-^F
  (ebk "\\eb"     ee-backward-word)                   ; Esc-b
  (ebk "\\eB"     ee-backward-word)                   ; Esc-B
  (ebk "\\e^B"    ee-backward-sexp)                   ; Esc-^B

  (ebk "^X^X"     ee-exchange-point-and-mark)         ; ^X^X
  (ebk "^X["      ee-backward-page)                   ; ^X[
  (ebk "^X]"      ee-forward-page)                    ; ^X]
  (ebk "\\e[5~"   ee-backward-page)                   ; Page-Up
  (ebk "\\e[6~"   ee-forward-page)                    ; Page-Down

  (ebk "^E"       ee-end-of-line)                     ; ^E
  (ebk "\\e[F"    ee-end-of-line)                     ; End key
 ; terminals are supposed to default to "normal" (aka "cursor") rather than
 ; "application" mode and in normal mode send ANSI \\e[F and \\e[H for End
 ; and Home.  although gnome terminal apparently starts in normal mode, it
 ; sends the application-mode sequences for this.  we capitulate reluctantly,
 ; since by defining Esc-OF and Esc-OH to do End and Home we prevent people
 ; from binding Esc-O by itself to a command.
  (ebk "\\eOF"    ee-end-of-line)                     ; End key (gnome terminal)
  (ebk "\\e[4~"   ee-end-of-line)                     ; End key (cygwin)
  (ebk "^A"       ee-beginning-of-line)               ; ^A
  (ebk "\\e[H"    ee-beginning-of-line)               ; Home key
  (ebk "\\eOH"    ee-beginning-of-line)               ; Home key (gnome terminal)
  (ebk "\\e[1~"   ee-beginning-of-line)               ; Home key (cygwin)
  (ebk "\\e<"     ee-beginning-of-entry)              ; Esc-<
  (ebk "\\e>"     ee-end-of-entry)                    ; Esc->      ; [[
  (ebk "\\e]"     ee-goto-matching-delimiter)         ; Esc-]
  (ebk #\(        ee-insert-paren)                    ; (
  (ebk #\)        ee-insert-paren)                    ; )
  (ebk #\[        ee-insert-paren)                    ; [
  (ebk #\]        ee-insert-paren)                    ; ]
  (ebk "^]"       ee-flash-matching-delimiter)        ; ^]

 ; destructive functions
  (ebk "^U"       ee-delete-line)                     ; ^U
  (ebk "^K"       ee-delete-to-eol)                   ; ^K
  (ebk "\\ek"     ee-delete-to-eol)                   ; Esc-k
  (ebk "^W"       ee-delete-between-point-and-mark)   ; ^W
  (ebk "^G"       ee-delete-entry)                    ; ^G
  (ebk "^C"       ee-reset-entry)                     ; ^C
  (ebk "\\e^K"    ee-delete-sexp)                     ; Esc-^K
  (ebk "\\e\\e[3~" ee-delete-sexp)                    ; Esc-Delete
  (ebk "\\e\177"  ee-backward-delete-sexp)            ; Esc-Backspace
  (ebk "\\e^H"    ee-backward-delete-sexp)            ; Esc-^H
  (ebk "^V"       ee-yank-selection)                  ; ^V
  (ebk "^Y"       ee-yank-kill-buffer)                ; ^Y
  (ebk "^D"       ee-eof/delete-char)                 ; ^D
  (ebk #\rubout   ee-backward-delete-char)            ; Backspace (<--)
  (ebk "\\e[3~"   ee-delete-char)                     ; Delete 
  (ebk "^H"       ee-backward-delete-char)            ; ^H
  (ebk "^@"       ee-set-mark)                        ; ^@ (or ^Space)
  (ebk "^^"       ee-set-mark)                        ; ^^

 ; display functions
  (ebk "^L"       ee-redisplay)                       ; ^L

 ; string macros
  (ebk "\\ed"     (ee-string-macro "(define "))       ; Esc-d   ; )
  (ebk "\\el"     (ee-string-macro "(lambda "))       ; Esc-l   ; )

 ; history keys
  (ebk "\\e^P"    ee-history-bwd)                     ; Esc-^P
  (ebk "\\e\\e[A" ee-history-bwd)                     ; Esc-Up
  (ebk "\\e^N"    ee-history-fwd)                     ; Esc-^N
  (ebk "\\e\\e[B" ee-history-fwd)                     ; Esc-Down
  (ebk "\\ep"     ee-history-bwd-prefix)              ; Esc-p
  (ebk "\\eP"     ee-history-bwd-contains)            ; Esc-P
  (ebk "\\en"     ee-history-fwd-prefix)              ; Esc-n
  (ebk "\\eN"     ee-history-fwd-contains)            ; Esc-N

 ; misc
  (ebk "\\e^U"   ee-command-repeat)                   ; Esc-^U
  (ebk "^Z"      ee-suspend-process)                  ; ^Z
)

(set! $expeditor
  (lambda (thunk)
    (let ([ee #f])
      (define (expeditor-prompt-and-read n)
        (if (cond
              [(eestate? ee) #t]
              [(eq? ee 'failed) #f]
              [(init-screen)
               (set! ee (make-eestate))
               (let ([histfile ($expeditor-history-file)])
                 (when histfile
                   (on-error (void) (ee-load-history ee histfile))))
               #t]
              [else (set! ee 'failed) #f])
            (ee-prompt-and-read ee n)
            (default-prompt-and-read n)))
      (let-values ([val* (parameterize ([waiter-prompt-and-read expeditor-prompt-and-read])
                           (thunk))])
        (when (eestate? ee)
          (let ([histfile ($expeditor-history-file)])
            (when histfile
              (on-error (void) (ee-save-history ee histfile)))))
        (apply values val*)))))
)

) ; when-feature expeditor
