diff --git a/ccw.core/plugin.properties b/ccw.core/plugin.properties index 0f407c1d..3f71a03f 100644 --- a/ccw.core/plugin.properties +++ b/ccw.core/plugin.properties @@ -82,7 +82,10 @@ ccw.ui.repl.history.previous.name=Load previous command from REPL's history into ccw.ui.repl.history.previous.description=Load previous command from REPL's history into REPL input area ccw.ui.repl.history.next.name=Load next command from REPL's history into REPL input area ccw.ui.repl.history.next.description=Load next command from REPL's history into REPL input area - +ccw.ui.repl.history.search.previous.name=Load previous command starting with the text before the cursor from REPL's history into REPL input area +ccw.ui.repl.history.search.previous.description=Load previous command starting with the text before the cursor from REPL's history into REPL input area +ccw.ui.repl.history.search.next.name=Load next command starting with the text before the cursor from REPL's history into REPL input area +ccw.ui.repl.history.search.next.description=Load next command starting with the text before the cursor from REPL's history into REPL input area preferencePage.clojure.name=Clojure preferencePage.general.name=General diff --git a/ccw.core/plugin.xml b/ccw.core/plugin.xml index f50be8a8..831a863c 100644 --- a/ccw.core/plugin.xml +++ b/ccw.core/plugin.xml @@ -463,6 +463,18 @@ id="ccw.ui.repl.history.next" name="%ccw.ui.repl.history.next.name"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )] [clojure.tools.nrepl.misc :only (uuid)]) (:import ccw.CCWPlugin @@ -109,6 +110,79 @@ (CCWPlugin/logError (eval-failure-msg nil expr) t) (log repl-view log-component (eval-failure-msg nil expr) :err)))) + +(defn next-history-entry + [history position retained-input backward? filter-pred cursor-split-text] + (let [cnt (count history), + ; -1 <= position < count + position (condp < position + -1 -1 + cnt position + (dec cnt)), + entries (if backward? + ; seq of history entries backward from current position + (map vector + (range (inc position) cnt) + (rseq (if (= position -1) + history + (subvec history 0 (dec (- (count history) position)))))) + ; seq of history entries forward from current position with retained input as last + (when-not (neg? position) + (map vector + (range (dec position) -1 -1) + (subvec history (- (count history) position)))))] + (or + (first (filter #(filter-pred cursor-split-text (second %)) entries)) + ; when not backward in history, then ensure retained-input as last match + (when-not backward? + [-1 retained-input])))) + + +(defn get-text-split-by-cursor + [^StyledText input-widget] + (let [cursor-pos (.getCaretOffset input-widget) + length (.getCharCount input-widget)] + (cond + (zero? length) + nil + (zero? cursor-pos) + [nil (.getText input-widget)] + (= cursor-pos length) + [(.getText input-widget) nil] + :else + [(.getText input-widget 0 (dec cursor-pos)) (.getText input-widget cursor-pos (dec length))]))) + +(defn history-entry + [history, history-pos] + (@history (- (count @history) @history-pos 1))) + +(defn search-history + [history history-position ^StyledText input-widget retained-input backward? modify-cursor filter-pred] + ; when there was a previous search and the history-entry was altered in the REPL, ... + (when (and @retained-input + (<= 0 @history-position) + (not= (history-entry history, history-position) (.getText input-widget))) + ; ... then reset the search by retaining the current input ... + (reset! retained-input (.getText input-widget)) + ; ... and reset the history position to start search from the end of the history + (reset! history-position -1)) + ; if no retained input, ... + (when-not @retained-input + ; ... search is starting now and the current input needs to be retained + (reset! retained-input (.getText input-widget))) + (if-let [[next-position, entry] (next-history-entry @history @history-position @retained-input + backward? filter-pred (get-text-split-by-cursor input-widget))] + (do + (reset! history-position next-position) + (when (= @retained-input entry) + (reset! retained-input nil)) + (let [cursor-pos (.getCaretOffset input-widget)] + (doto input-widget + (.setText entry) + (modify-cursor cursor-pos)))) + (beep))) + + (defn configure-repl-view [repl-view log-panel repl-client session-id] (let [[history retain-expr-fn] (history/get-history (-?> repl-view @@ -118,28 +192,11 @@ ; a bunch of atoms are just fine, since access to them is already ; serialized via the SWT event thread history (atom history) - current-step (atom -1) + history-position (atom -1) retained-input (atom nil) - history-action-fn - (fn [history-shift] - (swap! current-step history-shift) - (cond - (>= @current-step (count @history)) (do (swap! current-step dec) (beep)) - (neg? @current-step) (do (reset! current-step -1) - (when @retained-input - (doto input-widget - (.setText @retained-input) - cursor-at-end) - (reset! retained-input nil))) - :else (do - (when-not @retained-input - (reset! retained-input (.getText input-widget))) - (doto input-widget - (.setText (@history (dec (- (count @history) @current-step)))) - cursor-at-end)))) session-client (repl/client-session repl-client :session session-id) responses-promise (promise)] - (.setHistoryActionFn repl-view history-action-fn) + (.setHistoryActionFn repl-view (partial search-history history history-position input-widget retained-input)) ;; TODO need to make client-session accept a single arg to avoid ;; dummy message sends @@ -148,7 +205,7 @@ (comp (partial eval-expression repl-view log-panel session-client) (fn [expr add-to-log?] (reset! retained-input nil) - (reset! current-step -1) + (reset! history-position -1) (when add-to-log? (swap! history #(subvec ; don't add duplicate expressions to the history @@ -159,9 +216,27 @@ (retain-expr-fn expr)) expr)))) -(defn- load-history [event history-shift] +(defn- load-history [event backward? modify-cursor filter-pred] (let [repl-view (HandlerUtil/getActivePartChecked event)] - ((.getHistoryActionFn repl-view) history-shift))) + ((.getHistoryActionFn repl-view) backward? modify-cursor filter-pred))) + + +(defn move-cursor-to-end [input-widget _] (cursor-at-end input-widget)) + +(defn restore-cursor [^StyledText input-widget prev-cursor-pos] (.setCaretOffset input-widget prev-cursor-pos)) + + +(defn history-previous [_ event] (load-history event true move-cursor-to-end (constantly true))) +(defn history-next [_ event] (load-history event false move-cursor-to-end (constantly true))) + + +(defn text-begin-matches? + "Check whether the text begin matches the history entry. + If the text begin is blank, every history entry is matched (= classic stepping through history)." + [[text-begin] ^String history-entry] + (or + (str/blank? text-begin) + (.startsWith history-entry text-begin))) -(defn history-previous [_ event] (load-history event inc)) -(defn history-next [_ event] (load-history event dec)) +(defn history-backward-search [_ event] (load-history event true restore-cursor text-begin-matches?)) +(defn history-forward-search [_ event] (load-history event false restore-cursor text-begin-matches?))