text%: avoid O(n^2) behavior on string insert

Break large inserts into smaller chunks by successively halving the
chunk, instead of successviely peeling off a small part. Also, avoid
traversals and re-traversals of a long line while checking and
breaking up lines.
This commit is contained in:
Matthew Flatt 2014-11-24 11:52:05 -07:00
parent ce4f41bc72
commit ac5961eae9
2 changed files with 205 additions and 138 deletions

View File

@ -41,6 +41,7 @@
set-scroll-length
set-height
calc-line-length
adjust-line-length
set-starts-paragraph
starts-paragraph
adjust-max-width
@ -639,20 +640,7 @@
h mline-h set-mline-h!
mline-y set-mline-y!))
(define (calc-line-length mline)
(let ([l
(let ([nexts (snip->next (mline-last-snip mline))])
(let loop ([asnip (mline-snip mline)][l 0])
(if (eq? asnip nexts)
l
(let ([l (+ l (snip->count asnip))])
(when (has-flag? (snip->flags asnip) WIDTH-DEPENDS-ON-X)
(send asnip size-cache-invalid))
(loop (snip->next asnip) l)))))])
(when (not (= l (mline-len mline)))
(set-length mline l)))
(define (set-paragraph-ends mline)
(let ([next (mline-next mline)])
(cond
[(and next
@ -674,6 +662,27 @@
[(positive? (starts-paragraph mline))
(set-starts-paragraph mline #f)])))
(define (calc-line-length mline)
(let ([l
(let ([nexts (snip->next (mline-last-snip mline))])
(let loop ([asnip (mline-snip mline)][l 0])
(if (eq? asnip nexts)
l
(let ([l (+ l (snip->count asnip))])
(when (has-flag? (snip->flags asnip) WIDTH-DEPENDS-ON-X)
(send asnip size-cache-invalid))
(loop (snip->next asnip) l)))))])
(when (not (= l (mline-len mline)))
(set-length mline l)))
(set-paragraph-ends mline))
;; A scalable variant of `calc-line-lengt`, but doesn't
;; check WIDTH-DEPENDS-ON-X flags:
(define (adjust-line-length mline delta)
(set-length mline (+ (mline-len mline) delta))
(set-paragraph-ends mline))
(define (set-starts-paragraph mline starts?)
(unless (= (if starts? 1 0) (starts-paragraph mline))
(if starts?

View File

@ -1618,133 +1618,183 @@
(when (and prev
(not (has-flag? (snip->flags (mline-last-snip prev)) HARD-NEWLINE)))
(mline-mark-check-flow prev))))
;; If the snip is too large, divide it up:
(define-values (initial-snip initial-s)
(cond
[((+ s addlen) . > . MAX-COUNT-FOR-SNIP)
(let loop ([pos (- start s)] [snip snip] [size (+ addlen s)])
(cond
[(size . > . MAX-COUNT-FOR-SNIP)
(define half (quotient size 2))
(define intm-snip
(split-one half snip #f))
(define-values (next-snip next-pos)
(loop pos intm-snip half))
(loop next-pos next-snip (- size half))]
[else
(define new-snip (split-one* size snip))
(values (snip->next new-snip) (+ pos size))]))
(find-snip/pos start 'after)]
[else (values snip s)]))
;; The text is inserted, but all into one big snip. If the
;; inserted text contains any newlines or tabs, we need to split
;; it up to use tab snips or the HARD-NEWLINE flag:
(let loop ([snip-start-pos start]
[str (string-snip-buffer snip)]
[sp (+ s (string-snip-dtext snip))]
;; If the inserted text contains any newlines or tabs,
;; we need to split it up to use tab snips or the
;; HARD-NEWLINE flag:
(let loop ([the-snip initial-snip]
[the-s s]
[snip-start-pos start]
[str (string-snip-buffer initial-snip)]
[sp (+ s (string-snip-dtext initial-snip))]
[i 0]
[cnt 0]
[inserted-line? inserted-line?])
(if (= i addlen)
(begin
(set! first-line (mline-first (unbox line-root-box)))
(set! last-line (mline-last (unbox line-root-box)))
(set! len (+ len addlen))
(assert (= (last-position) (+ (mline-get-position last-line)
(mline-len last-line))))
(success-finish addlen inserted-line?))
(begin
(when (equal? (string-ref str sp) #\return)
(string-set! str sp #\newline))
(let ([c (string-ref str sp)])
(cond
[(or (equal? c #\newline) (equal? c #\tab))
(let ([newline? (equal? c #\newline)])
(make-snipset (+ i start) (+ i start 1))
(let ([snip (do-find-snip (+ i start) 'after)])
(if newline?
[inserted-line (and inserted-line?
(snip->line initial-snip))]
[sniplen (- (snip->count initial-snip) s)])
(cond
[(= i addlen)
(set! first-line (mline-first (unbox line-root-box)))
(set! last-line (mline-last (unbox line-root-box)))
(set! len (+ len addlen))
(assert (= (last-position) (+ (mline-get-position last-line)
(mline-len last-line))))
(when inserted-line
;; The last added line could have snips with WIDTH-DEPENDS-ON-X,
;; but we've only called `adjust-line-length` so far.
(mline-calc-line-length inserted-line))
(success-finish addlen (and inserted-line #t))]
[(= cnt sniplen)
;; move to next snip
(define snip (snip->next the-snip))
(loop snip
0
(+ i start)
(string-snip-buffer snip)
(string-snip-dtext snip)
i
0
inserted-line
(snip->count snip))]
[else
(when (equal? (string-ref str sp) #\return)
(string-set! str sp #\newline))
(define c (string-ref str sp))
(cond
[(or (equal? c #\newline) (equal? c #\tab))
(let ([newline? (equal? c #\newline)])
(define long-char-snip
(if (zero? (+ cnt the-s))
the-snip
(snip->next (split-one* (+ cnt the-s) the-snip))))
(define char-snip (split-one* 1 long-char-snip))
(define new-snip
(let ([snip char-snip])
(if newline?
;; forced return - split the snip
(begin
(set-snip-flags! snip
(remove-flag
(add-flag (add-flag (add-flag (snip->flags snip)
NEWLINE)
HARD-NEWLINE)
INVISIBLE)
CAN-APPEND))
(if (not (eq? snip (mline-last-snip (snip->line snip))))
(let* ([old-line (snip->line snip)]
[line (mline-insert old-line line-root-box #t)])
(set-snip-line! snip line)
(set! num-valid-lines (add1 num-valid-lines))
(set-mline-last-snip! line snip)
(set-mline-snip! line (mline-snip old-line))
;; forced return - split the line
(begin
(set-snip-flags! snip
(remove-flag
(add-flag (add-flag (add-flag (snip->flags snip)
NEWLINE)
HARD-NEWLINE)
INVISIBLE)
CAN-APPEND))
(if (not (eq? snip (mline-last-snip (snip->line snip))))
(let* ([old-line (snip->line snip)]
[line (mline-insert old-line line-root-box #t)])
(set-snip-line! snip line)
(set! num-valid-lines (add1 num-valid-lines))
(set-mline-last-snip! line snip)
(set-mline-snip! line (mline-snip old-line))
;; retarget snips moved to new line:
(let loop ([c-snip (mline-snip old-line)])
(unless (eq? c-snip snip)
(set-snip-line! c-snip line)
(loop (snip->next c-snip))))
(set-mline-snip! old-line (snip->next snip))
(mline-calc-line-length old-line)
(mline-mark-recalculate old-line)
(when (max-width . > . 0)
(mline-mark-check-flow old-line))
(mline-calc-line-length line)
(mline-mark-recalculate line)
(when (max-width . > . 0)
(mline-mark-check-flow line)))
;; carriage-return inserted at the end of a auto-wrapped line;
;; line lengths stay the same, but next line now starts
;; a paragraph
(let ([next (mline-next (snip->line snip))])
(when next
(when (zero? (mline-starts-paragraph next))
(mline-set-starts-paragraph next #t))))))
;; convert a tab to a tab-snip%
(let ([tabsnip (let ([ts (on-new-tab-snip)])
(if (or (send ts is-owned?)
(positive? (snip->count ts)))
;; uh-oh
(new tab-snip%)
ts))])
(set-snip-style! tabsnip (snip->style snip))
(let* ([rsnip (snip-set-admin tabsnip snip-admin)]
[tabsnip (if (not (eq? rsnip tabsnip))
;; uh-oh
(let ([tabsnip (new tab-snip%)])
(set-snip-style! tabsnip (snip->style snip))
(send tabsnip set-admin snip-admin)
tabsnip)
tabsnip)])
;; retarget snips moved to new line:
(define delta
(let loop ([c-snip (mline-snip old-line)] [delta 0])
(cond
[(eq? c-snip snip)
(+ delta (snip->count snip))]
[else
(set-snip-line! c-snip line)
(loop (snip->next c-snip)
(+ delta (snip->count c-snip)))])))
(set-snip-flags! tabsnip
(add-flag (snip->flags tabsnip) CAN-SPLIT))
(send tabsnip insert "\t" 1 0)
(when (has-flag? (snip->flags tabsnip) CAN-SPLIT)
(set-snip-flags! tabsnip
(remove-flag (snip->flags tabsnip) CAN-SPLIT)))
(when (has-flag? (snip->flags snip) NEWLINE)
(set-snip-flags! tabsnip (add-flag (snip->flags tabsnip) NEWLINE)))
(set-mline-snip! old-line (snip->next snip))
(splice-snip tabsnip (snip->prev snip) (snip->next snip))
(set-snip-line! tabsnip (snip->line snip))
(when (eq? (mline-snip (snip->line snip)) snip)
(set-mline-snip! (snip->line tabsnip) tabsnip))
(when (eq? (mline-last-snip (snip->line snip)) snip)
(set-mline-last-snip! (snip->line tabsnip) tabsnip))))))
(mline-adjust-line-length old-line (- delta))
(mline-mark-recalculate old-line)
(when (max-width . > . 0)
(mline-mark-check-flow old-line))
(let ([snip (do-find-snip (+ i start 1) 'after)])
(let ([i (add1 i)])
(loop (+ i start)
(if (= i addlen) #f (string-snip-buffer snip))
(if (= i addlen) #f (string-snip-dtext snip))
i
0
(or inserted-line? newline?)))))]
(mline-adjust-line-length line delta)
(mline-mark-recalculate line)
(when (max-width . > . 0)
(mline-mark-check-flow line)))
[(cnt . > . MAX-COUNT-FOR-SNIP)
;; divide up snip, because it's too large:
(make-snipset (+ i start) (+ i start))
(let ([snip (do-find-snip (+ i start) 'after)])
(loop (+ i start)
(string-snip-buffer snip)
(add1 (string-snip-dtext snip))
(add1 i)
1
inserted-line?))]
[else
(loop start str (+ sp 1) (+ i 1) (+ cnt 1) inserted-line?)])))))))))))
;; carriage-return inserted at the end of a auto-wrapped line;
;; line lengths stay the same, but next line now starts
;; a paragraph
(let ([next (mline-next (snip->line snip))])
(when next
(when (zero? (mline-starts-paragraph next))
(mline-set-starts-paragraph next #t)))))
snip)
;; convert a tab to a tab-snip%
(let ([tabsnip (let ([ts (on-new-tab-snip)])
(if (or (send ts is-owned?)
(positive? (snip->count ts)))
;; uh-oh
(new tab-snip%)
ts))])
(set-snip-style! tabsnip (snip->style snip))
(let* ([rsnip (snip-set-admin tabsnip snip-admin)]
[tabsnip (if (not (eq? rsnip tabsnip))
;; uh-oh
(let ([tabsnip (new tab-snip%)])
(set-snip-style! tabsnip (snip->style snip))
(send tabsnip set-admin snip-admin)
tabsnip)
tabsnip)])
(set-snip-flags! tabsnip
(add-flag (snip->flags tabsnip) CAN-SPLIT))
(send tabsnip insert "\t" 1 0)
(when (has-flag? (snip->flags tabsnip) CAN-SPLIT)
(set-snip-flags! tabsnip
(remove-flag (snip->flags tabsnip) CAN-SPLIT)))
(when (has-flag? (snip->flags snip) NEWLINE)
(set-snip-flags! tabsnip (add-flag (snip->flags tabsnip) NEWLINE)))
(splice-snip tabsnip (snip->prev snip) (snip->next snip))
(set-snip-line! tabsnip (snip->line snip))
(when (eq? (mline-snip (snip->line snip)) snip)
(set-mline-snip! (snip->line tabsnip) tabsnip))
(when (eq? (mline-last-snip (snip->line snip)) snip)
(set-mline-last-snip! (snip->line tabsnip) tabsnip))
tabsnip)))))
(let ([snip (snip->next new-snip)])
(let ([i (add1 i)])
(loop snip
0
(+ i start)
(if (= i addlen) #f (string-snip-buffer snip))
(if (= i addlen) #f (string-snip-dtext snip))
i
0
(if newline?
(snip->line (or snip new-snip))
inserted-line)
(if (= i addlen) 0 (snip->count snip))))))]
[else
(loop the-snip the-s
start str (+ sp 1) (+ i 1) (+ cnt 1)
inserted-line sniplen)])]))))))))
(define/private (check-len str len)
(unless (len . <= . (string-length str))
@ -4315,7 +4365,13 @@
(set-snip-flags! a (remove-flag (remove-flag (snip->flags b) NEWLINE)
HARD-NEWLINE)))))
(define/private (split-one pos s-pos snip extra)
(define/private (split-one* amt snip)
(if (or (= 0 amt)
(= amt (snip->count snip)))
snip
(split-one amt snip #f)))
(define/private (split-one amt snip extra)
(let ([line (snip->line snip)]
[prev (snip->prev snip)]
[next (snip->next snip)]
@ -4325,7 +4381,7 @@
[orig snip])
(let-boxes ([ins-snip #f]
[snip #f])
(snip-split orig (- pos s-pos) ins-snip snip)
(snip-split orig amt ins-snip snip)
(set-snip-style! snip style)
(set-snip-style! ins-snip style)
@ -4347,7 +4403,9 @@
(snip-set-admin snip snip-admin)
(snip-set-admin ins-snip snip-admin)
(after-split-snip (- pos s-pos))))))
(after-split-snip amt)
ins-snip))))
(define/private (make-snipset start end)
;; BEWARE: `len' may not be up-to-date
@ -4355,11 +4413,11 @@
(let-values ([(snip s-pos) (find-snip/pos start 'after-or-none)])
(when snip
(unless (= s-pos start)
(split-one start s-pos snip #f)))))
(split-one (- start s-pos) snip #f)))))
(when (positive? end)
(let-values ([(snip s-pos) (find-snip/pos end 'before)])
(unless (= (+ s-pos (snip->count snip)) end)
(split-one end s-pos snip #f)))))
(split-one (- end s-pos) snip #f)))))
(define/private (insert-text-snip start style)
(let* ([snip (on-new-string-snip)]
@ -4412,7 +4470,7 @@
(set-mline-snip! (snip->line snip) snip))
snip]
[else
(split-one start s-pos gsnip
(split-one (- start s-pos) gsnip
(lambda (gsnip)
(set-snip-line! snip (snip->line gsnip))
(insert-snip gsnip snip)))