Browse code

Merge pull request #50 from jbweston/feat/autocomplete

Add autocomplete

Closes #10, #11

Joseph Weston authored on 14/12/2021 06:11:56 • GitHub committed on 14/12/2021 06:11:56
Showing 5 changed files
... ...
@@ -32,6 +32,7 @@
32 32
 (def default-db
33 33
   {:name "DMR"
34 34
    :cmdline {:current ""
35
+             :suggestions {:options nil :selected nil}
35 36
              :selected-history nil
36 37
              :history []}
37 38
    :entities (fmap parse-srd-data srd-data)
... ...
@@ -1,6 +1,6 @@
1 1
 (ns dmr.events
2 2
   (:require
3
-   [clojure.string :refer [lower-case split join]]
3
+   [clojure.string :refer [lower-case split join starts-with?]]
4 4
    [re-frame.core :as re-frame]
5 5
    [dmr.db :as db]
6 6
    [day8.re-frame.tracing :refer-macros [fn-traced]]))
... ...
@@ -20,17 +20,30 @@
20 20
 (defn normalize-cmdline [cmdline]
21 21
     (-> cmdline single-space lower-case))
22 22
 
23
+(defn- concatv [m]
24
+    (->> m vals (apply concat)))
25
+
26
+(defn autocomplete-suggestions [db current]
27
+  (let [entities (db :entities)
28
+        cur (normalize-cmdline current)
29
+        best-match #(< (count %1) (count %2))]
30
+      (when (not-empty cur)
31
+        (->> (concatv entities)
32
+             (filter #(-> % :name normalize-cmdline (starts-with? cur)))
33
+             (map :name)
34
+             (sort best-match)))))
35
+
23 36
 (re-frame/reg-event-db
24 37
  ::update-cmdline
25 38
  (fn-traced [db [_ current]]
26
-  (assoc-in db [:cmdline :current] current)))
39
+  (-> db
40
+      (assoc-in [:cmdline :current] current)
41
+      (assoc-in [:cmdline :suggestions] {:options (autocomplete-suggestions db current)
42
+                                         :selected nil}))))
27 43
 
28 44
 (defn get-using [f coll]
29 45
   (->> coll (filter f) first))
30 46
 
31
-(defn- concatv [m]
32
-    (->> m vals (apply concat)))
33
-
34 47
 (defn execute [db cmdline]
35 48
  (let [append-history #(update-in db [:cmdline :history] conj {:input cmdline :output %})
36 49
        entities (db :entities)
... ...
@@ -63,7 +76,7 @@
63 76
         orig (some-> cs :selected-history :orig)
64 77
         idx (some-> cs :selected-history :index)
65 78
         history-end (- (count history) 1)
66
-        historical-input #(-> history (get %) :input)
79
+        historical-input #(-> history (nth %) :input)
67 80
         next (case direction :backward dec :forward inc)
68 81
         at-first? (= idx history-end)
69 82
         at-last?  (= idx 0)
... ...
@@ -73,8 +86,8 @@
73 86
     (cond
74 87
       (empty? history)                   {:current current :selected-history nil}
75 88
       (and at-first? going-forward?)     {:current orig :selected-history nil}
76
-      (and at-last? going-backward?)     {} ; do nothing
77
-      (and not-cycling? going-forward?)  {} ; do nothing
89
+      (and at-last? going-backward?)     nil ; do nothing
90
+      (and not-cycling? going-forward?)  nil ; do nothing
78 91
       (and not-cycling? going-backward?) {:current (historical-input history-end)
79 92
                                           :selected-history {:orig current
80 93
                                                              :index history-end}}
... ...
@@ -82,6 +95,26 @@
82 95
                                           :selected-history {:orig orig
83 96
                                                              :index (next idx)}})))
84 97
 
98
+(defn cycle-suggestions [cmd direction]
99
+  (let [{:keys [options selected]} (cmd :suggestions)
100
+        next (case direction :backward dec :forward inc)
101
+        nopts (count options)
102
+        next-selected (cond
103
+                        (nil? selected)     (case direction
104
+                                                  :backward (- nopts 1)
105
+                                                  :forward 0)
106
+                        (>= (next selected) nopts) 0
107
+                        (< (next selected) 0) (- nopts 1)
108
+                        :else               (next selected))]
109
+    (when (not-empty options)
110
+      {:suggestions {:options options :selected next-selected}})))
111
+
112
+(def null-suggestions {:options nil :selected nil})
113
+
114
+(defn complete-suggestion [{:keys [suggestions]}]
115
+  (let [{:keys [options selected]} suggestions]
116
+    {:current (nth options selected) :suggestions null-suggestions}))
117
+
85 118
 (re-frame/reg-event-db
86 119
  ::prompt-keypress
87 120
  (re-frame/path :cmdline)
... ...
@@ -89,11 +122,28 @@
89 122
    (let [direction (case (.-key event)
90 123
                      "ArrowUp"   :backward
91 124
                      "ArrowDown" :forward
92
-                     nil)]
93
-     (if direction
94
-       (do (.preventDefault event)
95
-           (merge cmd (cycle-history cmd direction)))
96
-       cmd))))
125
+                     "ArrowRight" :right
126
+                     nil)
127
+         tab (= (.-key event) "Tab")
128
+         escape (= (.-key event) "Escape")]
129
+     (println (.-key event) direction (contains? #{:backward :forward} direction))
130
+     (cond
131
+      (contains? #{:backward :forward} direction) (do (.preventDefault event)
132
+                                                      (if (some-> cmd :suggestions :options)
133
+                                                          (merge cmd (cycle-suggestions cmd direction))
134
+                                                          (merge cmd (cycle-history cmd direction))))
135
+      (and (= :right direction)
136
+           (some-> cmd :suggestions :selected)) (do (.preventDefault event)
137
+                                                    (merge cmd (complete-suggestion cmd)))
138
+      (and tab
139
+           (= 1 (some-> cmd :suggestions :options count))
140
+           (some-> cmd :suggestions :selected)) (do (.preventDefault event)
141
+                                                    (merge cmd (complete-suggestion cmd)))
142
+      tab (do (.preventDefault event)
143
+              (merge cmd (cycle-suggestions cmd :forward)))
144
+      (and escape (some-> cmd :suggestions :options)) (do (.preventDefault event)
145
+                                                          (merge cmd {:suggestions null-suggestions}))
146
+      :else     cmd))))
97 147
 
98 148
 (re-frame/reg-event-fx
99 149
  ::submit-cmd
... ...
@@ -102,8 +152,9 @@
102 152
         cmdline (some-> db :cmdline :current)]
103 153
     {:db
104 154
      (-> db
105
-      (update-in [:cmdline :current] (constantly nil))
106
-      (update-in [:cmdline :selected-history] (constantly nil))
155
+      (assoc-in [:cmdline :current] nil)
156
+      (assoc-in [:cmdline :selected-history] nil)
157
+      (assoc-in [:cmdline :suggestions] {:options nil :selected nil})
107 158
       (execute cmdline))
108 159
 
109 160
      ; dispatch-later to ensure that the DOM has already been updated,
... ...
@@ -202,4 +202,17 @@
202 202
     :padding 0
203 203
     :width (calc "100% - 2ch")}
204 204
    [:&:focus
205
-    {:outline :none}]])
205
+    {:outline :none}]]
206
+  [:ul.suggestions
207
+   {:position :absolute
208
+    :padding-left 0
209
+    :padding-right 0
210
+    :bottom 0
211
+    :left 0
212
+    :margin-bottom :1.2rem
213
+    :background-color (colorscheme :light0-soft)}
214
+   [:li {:list-style :none
215
+         :padding-left :3ch
216
+         :padding-right :1ch}
217
+     [(s/& :.selected)
218
+      {:background-color (colorscheme :neutral-purple)}]]])
... ...
@@ -12,6 +12,11 @@
12 12
  (fn [db]
13 13
    (get-in db [:cmdline :current])))
14 14
 
15
+(re-frame/reg-sub
16
+ ::cmdline-suggestions
17
+ (fn [db]
18
+   (get-in db [:cmdline :suggestions])))
19
+
15 20
 (re-frame/reg-sub
16 21
  ::cmd-history
17 22
  (fn [db]
... ...
@@ -8,24 +8,6 @@
8 8
    [dmr.subs :as subs]
9 9
    [dmr.events :as events]))
10 10
 
11
-(defn prompt []
12
-  (let [cmdline (re-frame/subscribe [::subs/cmdline])
13
-        update-cmdline #(re-frame/dispatch-sync
14
-                         [::events/update-cmdline (-> % .-target .-value)])
15
-        key-pressed #(re-frame/dispatch-sync [::events/prompt-keypress %])
16
-        submit-cmd #(do (.preventDefault %)
17
-                        (re-frame/dispatch [::events/submit-cmd (-> % .-target)]))]
18
-    [:form {:class (styles/prompt-style)
19
-            :on-submit submit-cmd}
20
-     [:input {:value @cmdline
21
-              :on-change update-cmdline
22
-              :on-keyDown key-pressed
23
-              :type "text"}]]))
24
-
25
-(defn title []
26
-  (let [name (re-frame/subscribe [::subs/name])]
27
-    [:h1 "Welcome to " @name]))
28
-
29 11
 ; TODO: move this functionto a separate 'utils' module
30 12
 (defn ordinal [n]
31 13
     (let [final-digit (mod n 10)
... ...
@@ -58,6 +40,33 @@
58 40
 
59 41
 (defn- enumerated [& colls] (apply zip (range) colls))
60 42
 
43
+
44
+(defn prompt []
45
+  (let [cmdline (re-frame/subscribe [::subs/cmdline])
46
+        suggestions (re-frame/subscribe [::subs/cmdline-suggestions])
47
+        update-cmdline #(re-frame/dispatch-sync
48
+                         [::events/update-cmdline (-> % .-target .-value)])
49
+        key-pressed #(re-frame/dispatch-sync [::events/prompt-keypress %])
50
+        submit-cmd #(do (.preventDefault %)
51
+                        (re-frame/dispatch [::events/submit-cmd (-> % .-target)]))]
52
+    [:form {:class (styles/prompt-style)
53
+            :on-submit submit-cmd}
54
+     [:input {:value @cmdline
55
+              :on-change update-cmdline
56
+              :on-keyDown key-pressed
57
+              :type "text"}]
58
+
59
+     [:ul.suggestions
60
+      (let [{:keys [options selected]} @suggestions]
61
+         (for [[idx x] (enumerated options)]
62
+           ^{:key x} [:li
63
+                      (when (= idx selected) {:class :selected})
64
+                      x]))]]))
65
+
66
+(defn title []
67
+  (let [name (re-frame/subscribe [::subs/name])]
68
+    [:h1 "Welcome to " @name]))
69
+
61 70
 (defn- property-list [& props]
62 71
     (into
63 72
      [:ul {:class (styles/property-list)}]