Skip to content

Commit 78bb005

Browse files
authored
paredit: kill-one-at-pos: word in string fix (#358)
In-string handling now: - no-op if pos does not locate to word (i.e. whitespace) - no longer has off-by-one error for word deletion Closes #343 Also contributes to #256
1 parent 89f5a99 commit 78bb005

File tree

4 files changed

+74
-32
lines changed

4 files changed

+74
-32
lines changed

CHANGELOG.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ A release with known breaking changes is marked with:
3535
{issue}351[#351] ({lread})
3636
** `split-at-pos` no longer throws on split at string opening quote
3737
{issue}350[#350] ({lread})
38+
** `kill-one-at-pos` word deletion in string/comment off-by-one error fixed
39+
{issue}343[#343] ({lread})
3840

3941
=== v1.1.49 - 2024-11-18 [[v1.1.49]]
4042

src/rewrite_clj/paredit.cljc

+40-23
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
[rewrite-clj.node :as nd]
77
[rewrite-clj.zip :as z]
88
[rewrite-clj.zip.findz :as fz]
9+
[rewrite-clj.zip.removez :as rz]
910
[rewrite-clj.zip.whitespace :as ws]))
1011

1112
#?(:clj (set! *warn-on-reflection* true))
@@ -146,26 +147,36 @@
146147
zloc))
147148

148149
(defn- find-word-bounds
149-
[v col]
150-
(when (<= col (count v))
151-
[(->> (seq v)
150+
"Return `[start-col end-col]` of word spanning 1-based `col` in `s`.
151+
Else nil if `col` is not in a word."
152+
[s col]
153+
(when (and (> col 0)
154+
(<= col (count s))
155+
(not (#{\space \newline} (nth s (dec col)))))
156+
[(->> s
152157
(take col)
153158
reverse
154-
(take-while #(not (= % \space))) count (- col))
155-
(->> (seq v)
159+
(take-while #(not (= % \space)))
160+
count
161+
(- col)
162+
inc)
163+
(->> s
156164
(drop col)
157165
(take-while #(not (or (= % \space) (= % \newline))))
158166
count
159167
(+ col))]))
160168

161169
(defn- remove-word-at
162-
[v col]
163-
(when-let [[start end] (find-word-bounds v col)]
164-
(str (subs v 0 start)
165-
(subs v end))))
170+
"Return `s` with word at 1-based `col` removed.
171+
If no word at `col` returns `s` unchanged"
172+
[s col]
173+
(if-let [[start end] (find-word-bounds s col)]
174+
(str (subs s 0 (dec start))
175+
(subs s end))
176+
s))
166177

167178
(defn- kill-word-in-comment-node [zloc pos]
168-
(let [col-bounds (-> zloc z/node meta :col)]
179+
(let [col-bounds (-> zloc z/position fz/pos-as-map :col)]
169180
(-> zloc
170181
(z/replace (-> zloc
171182
z/node
@@ -174,7 +185,7 @@
174185
nd/comment-node)))))
175186

176187
(defn- kill-word-in-string-node [zloc pos]
177-
(let [bounds (-> zloc z/node meta)
188+
(let [bounds (-> zloc z/position fz/pos-as-map)
178189
row-idx (- (:row pos) (:row bounds))
179190
col (if (= 0 row-idx)
180191
(- (:col pos) (:col bounds))
@@ -188,31 +199,37 @@
188199
nd/string-node)))))
189200

190201
(defn kill-one-at-pos
191-
"In string and comment aware kill for one node/word at `pos` in `zloc`.
202+
"Return `zloc` with node/word found at `pos` removed.
192203
193-
- `zloc` location is (inclusive) starting point for `pos` depth-first search
194-
- `pos` can be a `{:row :col}` map or a `[row col]` vector. The `row` and `col` values are
204+
If `pos` is:
205+
- inside a string or comment, removes word at `pos`, if at whitespace, no-op.
206+
- otherwise removes node and moves left, or if no left node removes via [[rewrite-clj.zip/remove]].
207+
If `pos` locates to whitespace between nodes, skips right to find node.
208+
209+
`zloc` location is (exclusive) starting point for `pos` search
210+
`pos` can be a `{:row :col}` map or a `[row col]` vector. The `row` and `col` values are
195211
1-based and relative to the start of the source code the zipper represents.
196212
197213
Throws if `zloc` was not created with [position tracking](/doc/01-user-guide.adoc#position-tracking).
198214
199-
- `(+ |100 100) => (+ |100)`
200-
- `(for |(bar do)) => (foo)`
215+
- `(+ |100 200) => (|+ 200)`
216+
- `(foo |(bar do)) => (foo)`
217+
- `[|10 20 30]` => |[20 30]`
201218
- `\"|hello world\" => \"| world\"`
202-
- ` ; |hello world => ; |world`"
219+
- `; |hello world => ; |world`"
203220
[zloc pos]
204221
(if-let [candidate (->> (z/find-last-by-pos zloc pos)
205222
(ws/skip z/right* ws/whitespace?))]
206223
(let [pos (fz/pos-as-map pos)
207-
[bounds-row bounds-col] (z/position candidate)
208-
kill-in-node? (not (and (= (:row pos) bounds-row)
209-
(<= (:col pos) bounds-col)))]
224+
candidate-pos (-> candidate z/position fz/pos-as-map)
225+
kill-in-node? (not (and (= (:row pos) (:row candidate-pos))
226+
(<= (:col pos) (:col candidate-pos))))]
210227
(cond
211228
(and kill-in-node? (string-node? candidate)) (kill-word-in-string-node candidate pos)
212229
(and kill-in-node? (ws/comment? candidate)) (kill-word-in-comment-node candidate pos)
213-
(not (z/leftmost? candidate)) (-> (z/remove candidate)
214-
(global-find-by-node (-> candidate z/left z/node)))
215-
:else (z/remove candidate)))
230+
:else
231+
(or (rz/remove-and-move-left candidate)
232+
(z/remove candidate))))
216233
zloc))
217234

218235
(defn- find-slurpee-up [zloc f]

src/rewrite_clj/zip/removez.cljc

+14
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@
8585
left-ws-trim
8686
right-ws-trim-keep-trailing-linebreak))
8787

88+
(defn remove-and-move-left
89+
"Return `zloc` with current node removed, and located to node left of removed node.
90+
If no left node, returns `nil`.
91+
92+
Currently internal, and likely not generic enough to expose, review update as necessary should we want to expose to public API."
93+
[zloc]
94+
(when (m/left zloc)
95+
(->> zloc
96+
left-ws-trim
97+
right-ws-trim
98+
u/remove-and-move-left
99+
;; TODO: needed?
100+
(ws/skip-whitespace zraw/left))))
101+
88102
(defn remove-preserve-newline
89103
"Same as [[remove]] but preserves newlines.
90104
Specifically: will trim all whitespace - or whitespace up to first linebreak if present."

test/rewrite_clj/paredit_test.cljc

+18-9
Original file line numberDiff line numberDiff line change
@@ -58,25 +58,34 @@
5858
;; for this pos fn test, ⊚ in `s` represents character row/col the the `pos`
5959
;; ⊚ in `expected` is at zipper node granularity
6060
(doseq [[s expected]
61-
[["[10⊚ 20 30]" "[⊚10 30]"]
61+
[["(+ ⊚100 200)" "(⊚+ 200)"]
62+
["(foo ⊚(bar do))" "(⊚foo)"]
63+
["[10⊚ 20 30]" "[⊚10 30]"] ;; searches forward for node
6264
["[10 ⊚20 30]" "[⊚10 30]"]
63-
["[[10]⊚ 20 30]" "[⊚[10] 30]"]
64-
["[[10] ⊚20 30]" "[⊚[10] 30]"]
65+
["[[10]⊚ 20 30]" "[⊚[10] 30]"] ;; searches forward for node
66+
["[[10] ⊚20 30]" "[⊚[10] 30]"] ;; navigates left after delete when possible
67+
["[10] [⊚20 30]" "[10] ⊚[30]"]
6568
["[⊚10\n 20\n 30]" "⊚[20\n 30]"]
6669
["[10\n⊚ 20\n 30]" "[⊚10\n 30]"]
6770
["[10\n 20\n⊚ 30]" "[10\n ⊚20]"]
71+
["[⊚10 20 30]" "⊚[20 30]"]
72+
["⊚[10 20 30]" ""]
73+
6874
;; in comment
69-
["; hello⊚ world" "⊚; hello "]
70-
["; hello ⊚world" "⊚; hello "]
71-
["; hello worl⊚d" "⊚; hello "]
72-
[";⊚ hello world" "⊚; world"]
75+
["; hello⊚ world" "⊚; hello world"] ;; only kill word if word spans pos
76+
["; hello ⊚world" "⊚; hello "] ;; at w of world, kill it
77+
["; ⊚hello world" "⊚; world"] ;; at h of hello, kill it
78+
["; hello worl⊚d" "⊚; hello "] ;; at d of world, kill it
79+
[";⊚ hello world" "⊚; hello world"] ;; not in any word, no-op ;;
80+
7381
;; in string
74-
["\"hello⊚ world\"" "\"hello \""]
82+
["\"hello⊚ world\"" "\"hello world\""] ;; not in word, no-op
7583
["\"hello ⊚world\"" "\"hello \""]
7684
["\"hello worl⊚d\"" "\"hello \""]
7785
["\"⊚hello world\"" "\" world\""]
7886
["\"⊚foo bar do\n lorem\"" "\" bar do\n lorem\""]
79-
["\"foo bar do\n⊚ lorem\"" "\"foo bar do\n \""]
87+
["\"foo bar do\n⊚ lorem\"" "\"foo bar do\n lorem\""] ;; not in word, no-op
88+
["\"foo bar do\n ⊚lorem\"" "\"foo bar do\n \""]
8089
["\"foo bar ⊚do\n lorem\"" "\"foo bar \n lorem\""]]]
8190
(let [{:keys [pos s]} (th/pos-and-s s)
8291
zloc (z/of-string* s {:track-position? true})]

0 commit comments

Comments
 (0)