Skip to content

Commit 1f20075

Browse files
author
Yannick Scherer
committed
make position-tracking zipper an optional feature.
This means that backwards-compatibility will be retained and the new features can be used where necessary.
1 parent ee5b5bd commit 1f20075

File tree

4 files changed

+237
-153
lines changed

4 files changed

+237
-153
lines changed

src/rewrite_clj/zip/utils.clj

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,37 @@
33

44
;; ## Remove
55

6+
(defn- update-in-path
7+
[[node path :as loc] k f]
8+
(let [v (get path k)]
9+
(if (seq v)
10+
(with-meta
11+
[node (assoc path k (f v) :changed? true)]
12+
(meta loc))
13+
loc)))
14+
615
(defn remove-right
716
"Remove right sibling of the current node (if there is one)."
8-
[{[r & rs] :right :as loc}]
9-
(assoc loc
10-
:right rs
11-
:changed? true))
17+
[loc]
18+
(if z/*active?*
19+
(let [{[r & rs] :right} loc]
20+
(assoc loc
21+
:right rs
22+
:changed? true))
23+
(update-in-path loc :r next)))
1224

1325
(defn remove-left
1426
"Remove left sibling of the current node (if there is one)."
15-
[{:keys [left] :as loc}]
16-
(if-let [[_ lpos] (peek left)]
17-
(assoc loc
18-
:left (pop left)
19-
:position lpos
20-
:changed? true)
21-
loc))
27+
[loc]
28+
(if z/*active?*
29+
(let [{:keys [left]} loc]
30+
(if-let [[_ lpos] (peek left)]
31+
(assoc loc
32+
:left (pop left)
33+
:position lpos
34+
:changed? true)
35+
loc))
36+
(update-in-path loc :l pop)))
2237

2338
(defn remove-right-while
2439
"Remove elements to the right of the current zipper location as long as
@@ -47,21 +62,40 @@
4762
(defn remove-and-move-left
4863
"Remove current node and move left. If current node is at the leftmost
4964
location, returns `nil`."
50-
[{:keys [position left] :as loc}]
51-
(if (seq left)
52-
(let [[lnode lpos] (peek left)]
53-
(assoc loc
54-
:changed? true
55-
:node lnode
56-
:position lpos
57-
:left (pop left)))))
65+
[loc]
66+
(if z/*active?*
67+
(let [{:keys [position left]} loc]
68+
(if (seq left)
69+
(let [[lnode lpos] (peek left)]
70+
(assoc loc
71+
:changed? true
72+
:node lnode
73+
:position lpos
74+
:left (pop left)))))
75+
(let [[_ {:keys [l] :as path}] loc]
76+
(if (seq l)
77+
(with-meta
78+
[(peek l) (-> path
79+
(update-in [:l] pop)
80+
(assoc :changed? true))]
81+
(meta loc))))))
5882

5983
(defn remove-and-move-right
6084
"Remove current node and move right. If current node is at the rightmost
6185
location, returns `nil`."
62-
[{:keys [position right] :as loc}]
63-
(if (seq right)
64-
(assoc loc
65-
:changed? true
66-
:node (first right)
67-
:right (next right))))
86+
[loc]
87+
(if z/*active?*
88+
(let [{:keys [position right]} loc]
89+
(if (seq right)
90+
(assoc loc
91+
:changed? true
92+
:node (first right)
93+
:right (next right))))
94+
95+
(let [[_ {:keys [r] :as path}] loc]
96+
(if (seq r)
97+
(with-meta
98+
[(first r) (-> path
99+
(update-in [:r] next)
100+
(assoc :changed? true))]
101+
(meta loc))))))

src/rewrite_clj/zip/zip.clj

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,73 @@
1414
:author "Rich Hickey"}
1515
rewrite-clj.zip.zip
1616
(:refer-clojure :exclude (replace remove next))
17-
(:require [rewrite-clj.node.protocols :as node]))
17+
(:require [rewrite-clj.node.protocols :as node]
18+
[clojure.zip :as clj-zip]))
19+
20+
;; ## Switch
21+
;;
22+
;; To not force users into using this custom zipper, the following flag
23+
;; is used to dispatch to `clojure.zip` when set to `false`.
24+
25+
(def ^:dynamic *active?*
26+
"Set this to true to activate the custom, position-tracking zipper
27+
implementation."
28+
false)
29+
30+
(defmacro ^:private defn-switchable
31+
[sym docstring params & body]
32+
(let [placeholders (repeatedly (count params) gensym)]
33+
`(defn ~sym
34+
~docstring
35+
[~@placeholders]
36+
(if *active?*
37+
(let [~@(interleave params placeholders)]
38+
~@body)
39+
(~(symbol "clojure.zip" (name sym)) ~@placeholders)))))
40+
41+
(defmacro with-positional-zipper
42+
"Do not use `clojure.zip` to evaluate any rewrite-clj zipper operations in
43+
`body` but a custom position-tracking zipper that offers the function
44+
`position` to return row and column of a node."
45+
[& body]
46+
`(binding [*active?* true]
47+
~@body))
48+
49+
;; ## Implementation
1850

1951
(defn ^:no-doc zipper
2052
"Creates a new zipper structure."
2153
[root]
22-
{:node root
23-
:position [1 1]
24-
:parent nil
25-
:left []
26-
:right '()})
54+
(if *active?*
55+
{:node root
56+
:position [1 1]
57+
:parent nil
58+
:left []
59+
:right '()}
60+
(clj-zip/zipper
61+
node/inner?
62+
(comp seq node/children)
63+
node/replace-children
64+
root)))
2765

28-
(defn node
66+
(defn-switchable node
2967
"Returns the node at loc"
3068
[{:keys [node]}]
3169
node)
3270

33-
(defn branch?
71+
(defn-switchable branch?
3472
"Returns true if the node at loc is a branch"
3573
[{:keys [node]}]
3674
(node/inner? node))
3775

38-
(defn children
76+
(defn-switchable children
3977
"Returns a seq of the children of node at loc, which must be a branch"
4078
[{:keys [node] :as loc}]
4179
(if (branch? loc)
4280
(seq (node/children node))
4381
(throw (Exception. "called children on a leaf node"))))
4482

45-
(defn ^:no-doc make-node
83+
(defn-switchable ^:no-doc make-node
4684
"Returns a new branch node, given an existing node and new
4785
children. The loc is only used to supply the constructor."
4886
[loc node children]
@@ -51,14 +89,19 @@
5189
(defn position
5290
"Returns the ones-based [row col] of the start of the current node"
5391
[loc]
54-
(:position loc))
92+
(if *active?*
93+
(:position loc)
94+
(throw
95+
(IllegalStateException.
96+
(str "to use the positional zipper functions, please use "
97+
"`rewrite-clj.zip.zip/with-positional-zipper`.")))))
5598

56-
(defn lefts
99+
(defn-switchable lefts
57100
"Returns a seq of the left siblings of this loc"
58101
[loc]
59102
(map first (:left loc)))
60103

61-
(defn down
104+
(defn-switchable down
62105
"Returns the loc of the leftmost child of the node at this loc, or
63106
nil if no children"
64107
[loc]
@@ -72,7 +115,7 @@
72115
:left []
73116
:right cnext}))))
74117

75-
(defn up
118+
(defn-switchable up
76119
"Returns the loc of the parent of the node at this loc, or nil if at
77120
the top"
78121
[loc]
@@ -86,7 +129,7 @@
86129
(concat (map first left) (cons node right))))
87130
parent))))
88131

89-
(defn root
132+
(defn-switchable root
90133
"zips all the way up and returns the root node, reflecting any changes."
91134
[{:keys [end?] :as loc}]
92135
(if end?
@@ -96,7 +139,7 @@
96139
(recur p)
97140
(node loc)))))
98141

99-
(defn right
142+
(defn-switchable right
100143
"Returns the loc of the right sibling of the node at this loc, or nil"
101144
[loc]
102145
(let [{:keys [node parent position left] [r & rnext :as right] :right} loc]
@@ -107,14 +150,14 @@
107150
:right rnext
108151
:position (node/+extent position (node/extent node))))))
109152

110-
(defn rightmost
153+
(defn-switchable rightmost
111154
"Returns the loc of the rightmost sibling of the node at this loc, or self"
112155
[loc]
113156
(if-let [next (right loc)]
114157
(recur next)
115158
loc))
116159

117-
(defn left
160+
(defn-switchable left
118161
"Returns the loc of the left sibling of the node at this loc, or nil"
119162
[loc]
120163
(let [{:keys [node parent left right]} loc]
@@ -126,7 +169,7 @@
126169
:left (pop left)
127170
:right (cons node right))))))
128171

129-
(defn leftmost
172+
(defn-switchable leftmost
130173
"Returns the loc of the leftmost sibling of the node at this loc, or self"
131174
[loc]
132175
(let [{:keys [node parent left right]} loc]
@@ -139,7 +182,7 @@
139182
:right (concat (map first (rest left)) [node] right)))
140183
loc)))
141184

142-
(defn insert-left
185+
(defn-switchable insert-left
143186
"Inserts the item as the left sibling of the node at this loc,
144187
without moving"
145188
[loc item]
@@ -151,7 +194,7 @@
151194
:left (conj left [item position])
152195
:position (node/+extent position (node/extent item))))))
153196

154-
(defn insert-right
197+
(defn-switchable insert-right
155198
"Inserts the item as the right sibling of the node at this loc,
156199
without moving"
157200
[loc item]
@@ -162,29 +205,31 @@
162205
:changed? true
163206
:right (cons item right)))))
164207

165-
(defn replace
208+
(defn-switchable replace
166209
"Replaces the node at this loc, without moving"
167210
[loc node]
168211
(assoc loc :changed? true :node node))
169212

170213
(defn edit
171214
"Replaces the node at this loc with the value of (f node args)"
172215
[loc f & args]
173-
(replace loc (apply f (node loc) args)))
216+
(if *active?*
217+
(replace loc (apply f (node loc) args))
218+
(apply clj-zip/edit loc f args)))
174219

175-
(defn insert-child
220+
(defn-switchable insert-child
176221
"Inserts the item as the leftmost child of the node at this loc,
177222
without moving"
178223
[loc item]
179224
(replace loc (make-node loc (node loc) (cons item (children loc)))))
180225

181-
(defn append-child
226+
(defn-switchable append-child
182227
"Inserts the item as the rightmost child of the node at this loc,
183228
without moving"
184229
[loc item]
185230
(replace loc (make-node loc (node loc) (concat (children loc) [item]))))
186231

187-
(defn next
232+
(defn-switchable next
188233
"Moves to the next loc in the hierarchy, depth-first. When reaching
189234
the end, returns a distinguished loc detectable via end?. If already
190235
at the end, stays there."
@@ -199,7 +244,7 @@
199244
(or (right (up p)) (recur (up p)))
200245
(assoc p :end? true))))))
201246

202-
(defn prev
247+
(defn-switchable prev
203248
"Moves to the previous loc in the hierarchy, depth-first. If already
204249
at the root, returns nil."
205250
[loc]
@@ -210,12 +255,12 @@
210255
loc))
211256
(up loc)))
212257

213-
(defn end?
258+
(defn-switchable end?
214259
"Returns true if loc represents the end of a depth-first walk"
215260
[loc]
216261
(:end? loc))
217262

218-
(defn remove
263+
(defn-switchable remove
219264
"Removes the node at loc, returning the loc that would have preceded
220265
it in a depth-first walk."
221266
[loc]

test/rewrite_clj/zip/utils_test.clj

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,32 @@
2929

3030
(tabular
3131
(fact "`remove-and-move-left` tracks current position correctly"
32-
(let [root (base/of-string "[a bb ccc]")
33-
zloc (nth (iterate z/next root) ?n)]
34-
(z/position (remove-and-move-left zloc)) => ?pos))
32+
(z/with-positional-zipper
33+
(let [root (base/of-string "[a bb ccc]")
34+
zloc (nth (iterate z/next root) ?n)]
35+
(z/position (remove-and-move-left zloc)) => ?pos)))
3536
?n ?pos
3637
3 [1 3]
3738
5 [1 6]
3839
2 [1 2])
3940

4041
(tabular
4142
(fact "`remove-and-move-right` does not affect position"
42-
(let [root (base/of-string "[a bb ccc]")
43-
zloc (nth (iterate z/next root) ?n)]
44-
(z/position (remove-and-move-right zloc)) => ?pos))
43+
(z/with-positional-zipper
44+
(let [root (base/of-string "[a bb ccc]")
45+
zloc (nth (iterate z/next root) ?n)]
46+
(z/position (remove-and-move-right zloc)) => ?pos)))
4547
?n ?pos
4648
3 [1 4]
4749
1 [1 2]
4850
2 [1 3])
4951

5052
(tabular
5153
(fact "`remove-left` tracks current position correctly"
52-
(let [root (base/of-string "[a bb ccc]")
53-
zloc (nth (iterate z/next root) ?n)]
54-
(z/position (remove-left zloc)) => ?pos))
54+
(z/with-positional-zipper
55+
(let [root (base/of-string "[a bb ccc]")
56+
zloc (nth (iterate z/next root) ?n)]
57+
(z/position (remove-left zloc)) => ?pos)))
5558
?n ?pos
5659
3 [1 3]
5760
5 [1 6])

0 commit comments

Comments
 (0)