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?))