Module: deuce-internals Synopsis: The Deuce editor Author: Scott McKay Copyright: Original Code is Copyright (c) 1995-2004 Functional Objects, Inc. All rights reserved. License: Functional Objects Library Public License Version 1.0 Dual-license: GNU Lesser General Public License Warranty: Distributed WITHOUT WARRANTY OF ANY KIND /// Basic history support define variable $default-history-length :: = 40; define constant = one-of(#"clipboard", #"yank", #"yank-next"); define open abstract primary class () sealed constant slot %lock :: = make(); sealed slot %elements :: = #(); sealed slot %current-length :: = 0; sealed slot %maximum-length :: false-or() = $default-history-length, init-keyword: maximum-length:; sealed slot %rotation :: = 0; // state for 'yank-next' sealed slot %position :: = 0; // 'yank' to 'yank-next' communication sealed slot %temporary-element = #f; // can be bound to temporary front of list // Where should 'yank-next' get the next element from? sealed slot %yank-state :: = #"yank", init-keyword: yank-state:; end class ; define sealed method initialize-yank-state (history :: ) => () history.%yank-state := #"yank" end method initialize-yank-state; // Number of elements in the history, including the temporary element define sealed method history-length (history :: ) => (length :: ) if (history.%temporary-element) history.%current-length + 1 else history.%current-length end end method history-length; // Get the element from HISTORY indexed by INDEX define sealed method history-element (history :: , index :: , #key fixup?) => (elt, index :: false-or()) block (return) let delta :: = 0; with-lock (history.%lock) when (history.%temporary-element) when (index = 0 | (fixup? & index < 0)) return(history.%temporary-element, 0) end; inc!(index); dec!(delta) end; let elements = history.%elements; case index < 0 => values(fixup? & ~empty?(elements) & head(elements), delta); index < history.%current-length => values(~empty?(elements) & elements[index], index + delta); fixup? => values(~empty?(elements) & last(elements), history.%current-length + delta - 1); otherwise => values(#f, #f); end end end end method history-element; // Walk over the elements in the history, starting at INDEX offset by OFFSET, // continuing CUTOFF-LENGTH times. FUNCTION gets called with two arguments, // the element and its index. If TEST is supplied, it should be a function // of one argument, the element. define sealed method do-history-elements (function :: , history :: , #key index, offset :: = 0, cutoff-length, test) let index :: = case ~index => offset; index > 0 => offset + index - 1; otherwise => offset + index; end; let length = history-length(history); let n :: = if (cutoff-length) min(length, cutoff-length) else length end; for (i :: from 0 below n) unless (-1 < index & index < length) index := modulo(index, length) end; let elt = history-element(history, index); when (history-element-visible?(history, elt) & (~test | test(elt))) function(elt, index) end; inc!(index) end end method do-history-elements; // This is the guts of c-Y/c-m-Y, or c-sh-Y/c-m-sh-Y. // Returns an element, or #f if it can't find one (that matches). // INDEX is the index at which to start looking for a matching element. // If TEST is supplied, it should be a function of one argument, an element. define method yank-from-history (history :: , #key index, test) => (elt) block (return) let idx :: = case ~index => history.%rotation; index > 0 => index - 1; index < 0 => index; otherwise => error("Zero should have been handled at a higher level"); end; let fixup? = #t; with-lock (history.%lock) history.%yank-state := #"yank-next"; while (#t) let (elt, position) = history-element(history, idx, fixup?: fixup?); case ~elt => return(#f); (index | history-element-visible?(history, elt)) & (~test | test(elt)) => history.%position := position; return(elt); end; inc!(idx); fixup? := #f end end end end method yank-from-history; // This is the guts of m-Y or m-sh-Y (having already figured out that the last // command was a yanking command and which history it used). // Returns #f if it fails to find anything different. // INDEX and TEST are as for 'yank-from-history'. define method yank-next-from-history (history :: , #key index :: = 1, test) => (elt) block (return) when (index = 0) error("Zero should have been handled at a higher level") end; when (history.%position) with-lock (history.%lock) history.%yank-state := #"yank-next"; let old-elt = history-element(history, history.%position); local method do-element (elt, idx :: ) unless (history-elements-equal?(history, elt, old-elt)) history.%rotation := idx; history.%position := idx; return(elt) end end method; do-history-elements(do-element, history, index: history.%position + 1, offset: index, test: test) end end end end method yank-next-from-history; // Push an element onto the front of the history and extend its length define method history-push (history :: , elt) => () with-lock (history.%lock) let top-elt = history-top(history); // It saves space and is a nicer user interface if we don't push the new // element if it is equal to the element currently on top unless (top-elt & history-elements-equal?(history, elt, top-elt)) if (history.%current-length = history.%maximum-length) let last-pair = begin let last-pair = history.%elements; for (i :: from 0 below history.%current-length - 3) last-pair := tail(last-pair) end; last-pair end; push!(history.%elements, elt); tail(last-pair) := list(head(tail(last-pair))) else push!(history.%elements, elt); inc!(history.%current-length) end end; history.%rotation := 0; history.%temporary-element := #f end end method history-push; // Pop the top element from the history, and reduce its length. define method history-pop (history :: ) => (elt) with-lock (history.%lock) when (history.%current-length > 0) dec!(history.%current-length); let elt = head(history.%elements); history.%elements := tail(history.%elements); elt end end end method history-pop; define method history-top (history :: ) => (elt) ~empty?(history.%elements) & head(history.%elements) end method history-top; // Replace the top element of the history with a new element. define method history-top-setter (elt, history :: ) => (elt) head(history.%elements) := elt end method history-top-setter; define method history-elements-equal? (history :: , elt1, elt2) => (equal? :: ) // Note that on strings, this does a case-sensitive comparison elt1 = elt2 end method history-elements-equal?; // Is this element visible in the history? The default answer is yes define method history-element-visible? (history :: , elt) => (visible? :: ) ignore(elt); #t end method history-element-visible?; define method reset-history (history :: ) => () with-lock (history.%lock) history.%elements := #(); history.%current-length := 0; history.%position := 0; history.%rotation := 0; history.%temporary-element := #f; initialize-yank-state(history) end end method reset-history; /// Kill ring history define sealed class () keyword yank-state: = #"clipboard"; end class ; define sealed domain make (singleton()); define sealed domain initialize (); define sealed method initialize-yank-state (history :: ) => () history.%yank-state := #"clipboard" end method initialize-yank-state; /// Kill ring elements define method history-element-size (elt :: ) => (size :: ) size(elt) end method history-element-size; define method history-element-size (elt :: ) => (size :: ) count-characters(elt) end method history-element-size; define method merge-history-elements (elt1 :: , elt2 :: ) => (result :: ) concatenate-as(, elt1, elt2) end method merge-history-elements; define method merge-history-elements (elt1 :: , elt2 :: ) => (result :: ) concatenate-as(, elt1, as(, elt2)) end method merge-history-elements; define method merge-history-elements (elt1 :: , elt2 :: ) => (result :: ) concatenate-as(, as(, elt1), elt2) end method merge-history-elements; // Note that this doesn't copy the intervals! define method merge-history-elements (elt1 :: , elt2 :: ) => (result :: ) let end1 :: = interval-end-bp(elt1); let start2 :: = interval-start-bp(elt2); if (bp-character(end1) = '\n') let last1 = bp-line(end1); let first2 = bp-line(start2); // Link up the two intervals line-next(last1) := first2; line-previous(first2) := last1; interval-end-bp(elt1) := interval-end-bp(elt2) else let bp = insert!(end1, elt2); move-bp!(end1, bp-line(bp), bp-index(bp)) end; elt1 end method merge-history-elements; /// Higher level kill ring functions define method add-to-kill-ring (kill-ring :: , object :: type-union(, ), #key merge? = #f, reverse? = #f) => (elt) let top-elt = merge? & history-top(kill-ring); let new-elt = if (instance?(object, )) object else let interval :: = object; // force tighter type... if (bp-line(interval-start-bp(interval)) == bp-line(interval-end-bp(interval))) as(, interval) // more efficient for simple things... else copy-interval(interval) end end; let new-elt = if (top-elt) if (reverse?) merge-history-elements(new-elt, top-elt) else merge-history-elements(top-elt, new-elt) end else new-elt end; if (top-elt) history-top(kill-ring) := new-elt else history-push(kill-ring, new-elt) end; new-elt end method add-to-kill-ring; // This does the "next" kind of yank depending on the history's yank state define method yank-from-kill-ring (kill-ring :: , window :: , #key index) => (elt :: false-or(type-union(, ))) let policy = editor-policy(frame-editor(window-frame(window))); let clipboard? = clipboard-policy(policy); let clipboard = clipboard? & get-from-clipboard(window, ); let state = kill-ring.%yank-state; case clipboard? & state == #"clipboard" & ~index => kill-ring.%yank-state := #"yank"; clipboard; state == #"yank-next" => yank-next-from-history(kill-ring, index: index | 1); otherwise => kill-ring.%yank-state := #"yank-next"; let elt = yank-from-history(kill-ring, index: index | 1); if (clipboard? & elt = clipboard) yank-next-from-history(kill-ring, index: index | 1) else elt end end end method yank-from-kill-ring;