Skip to content

Commit 8a7d7c2

Browse files
committed
feat: [experimental] incremental treesitter selection
API (semi-public): - Targets can have an `endpos` field (the same label is used). - Added `traversal` parameter to `leap()` (allowing traversal for custom actions). Closes #132
1 parent 805cd3a commit 8a7d7c2

File tree

6 files changed

+377
-44
lines changed

6 files changed

+377
-44
lines changed

fnl/leap/beacons.fnl

+22-16
Original file line numberDiff line numberDiff line change
@@ -199,25 +199,31 @@ an autojump. (In short: always err on the safe side.)
199199
(tset unlabeled-match-positions (->key col-ch2) target)))))))
200200

201201

202+
(fn light-up-beacon [target endpos?]
203+
(let [[lnum col] (or (and endpos? target.endpos) target.pos)
204+
bufnr target.wininfo.bufnr
205+
[offset virttext] target.beacon
206+
opts {:virt_text virttext
207+
:virt_text_pos (or opts.virt_text_pos "overlay")
208+
:hl_mode "combine"
209+
:priority hl.priority.label}]
210+
(local id (api.nvim_buf_set_extmark
211+
bufnr hl.ns (- lnum 1) (+ col -1 offset) opts))
212+
; Register each newly set extmark in a table, so that we can delete
213+
; them one by one, without needing any further contextual
214+
; information. This is relevant if we process user-given targets and
215+
; have no knowledge about the boundaries of the search area.
216+
(table.insert hl.extmarks [bufnr id])))
217+
218+
202219
(fn light-up-beacons [targets ?start ?end]
203-
(when (or (not opts.on_beacons)
204-
(opts.on_beacons targets ?start ?end))
220+
(when (or (not opts.on_beacons) (opts.on_beacons targets ?start ?end))
205221
(for [i (or ?start 1) (or ?end (length targets))]
206222
(local target (. targets i))
207-
(case target.beacon
208-
[offset virttext]
209-
(let [bufnr target.wininfo.bufnr
210-
[lnum col] (map dec target.pos) ; 1/1 -> 0/0 indexing
211-
id (api.nvim_buf_set_extmark bufnr hl.ns lnum (+ col offset)
212-
{:virt_text virttext
213-
:virt_text_pos "overlay"
214-
:hl_mode "combine"
215-
:priority hl.priority.label})]
216-
; Register each newly set extmark in a table, so that we can
217-
; delete them one by one, without needing any further contextual
218-
; information. This is relevant if we process user-given targets
219-
; and have no knowledge about the boundaries of the search area.
220-
(table.insert hl.extmarks [bufnr id]))))))
223+
(when target.beacon
224+
(light-up-beacon target)
225+
(when target.endpos
226+
(light-up-beacon target true))))))
221227

222228

223229
{: set-beacons

fnl/leap/main.fnl

+16-13
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ char separately.
250250
:target_windows target-windows
251251
:opts user-given-opts
252252
:targets user-given-targets
253-
:action user-given-action}
253+
:action user-given-action
254+
:traversal action-can-traverse?}
254255
kwargs)
255256
(local {:backward backward?}
256257
(if invoked-dot-repeat? state.dot_repeat
@@ -377,9 +378,10 @@ char separately.
377378
(vim.cmd :redraw))
378379

379380
(fn can-traverse? [targets]
380-
(and directional?
381-
(not (or count op-mode? user-given-action))
382-
(>= (length targets) 2)))
381+
(or action-can-traverse?
382+
(and directional?
383+
(not (or count op-mode? user-given-action))
384+
(>= (length targets) 2))))
383385

384386
; When traversing without labels, keep highlighting the same one group
385387
; of targets, and do not shift until reaching the end of the group - it
@@ -483,11 +485,12 @@ char separately.
483485
; Sets `autojump` and `label_set` attributes for the target list, plus
484486
; `label` and `group` attributes for each individual target.
485487
(fn prepare-labeled-targets* [targets]
486-
(local force-noautojump? (or
487-
; No jump, doing sg else.
488-
user-given-action
489-
; Should be able to select our target.
490-
(and op-mode? (> (length targets) 1))))
488+
(local force-noautojump? (and (not action-can-traverse?)
489+
(or
490+
; No jump, doing sg else.
491+
user-given-action
492+
; Should be able to select our target.
493+
(and op-mode? (> (length targets) 1)))))
491494
(prepare-labeled-targets targets force-noautojump? multi-window-search?))
492495

493496
; Repeat
@@ -542,6 +545,8 @@ char separately.
542545
: inclusive-op?})
543546
(set first-jump? false))))
544547

548+
(local do-action (or user-given-action jump-to!))
549+
545550
; Target-selection loops
546551

547552
(fn post-pattern-input-loop [targets]
@@ -617,20 +622,18 @@ char separately.
617622
(vim.fn.feedkeys in :i)
618623
(case (get-new-idx idx in)
619624
new-idx (do
620-
(jump-to! (. targets new-idx))
625+
(do-action (. targets new-idx))
621626
(loop new-idx false))
622627
; We still want the labels (if there are) to function.
623628
_ (case (get-target-with-active-label targets in)
624-
target (jump-to! target)
629+
target (do-action target)
625630
_ (vim.fn.feedkeys in :i))))))
626631

627632
(loop start-idx true))
628633

629634
; //> Helper functions END
630635

631636

632-
(local do-action (or user-given-action jump-to!))
633-
634637
; After all the stage-setting, here comes the main action you've all been
635638
; waiting for:
636639

fnl/leap/treesitter.fnl

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
(local api vim.api)
2+
3+
4+
(fn get-nodes []
5+
(if (not (pcall vim.treesitter.get_parser))
6+
(values nil "No treesitter parser for this filetype.")
7+
(case (vim.treesitter.get_node)
8+
node
9+
(let [nodes [node]]
10+
(var parent (node:parent))
11+
(while parent
12+
(table.insert nodes parent)
13+
(set parent (parent:parent)))
14+
nodes))))
15+
16+
17+
(fn nodes->targets [nodes]
18+
(local linewise? (: (vim.fn.mode true) :match "V"))
19+
(local targets [])
20+
; To skip duplicate ranges.
21+
(var prev-range [])
22+
(var prev-line-range [])
23+
(each [_ node (ipairs nodes)]
24+
(let [(startline startcol endline endcol) (node:range) ; (0,0)
25+
range [startline startcol endline endcol]
26+
line-range [startline endline]
27+
remove-prev? (if linewise?
28+
(vim.deep_equal line-range prev-line-range)
29+
(vim.deep_equal range prev-range))]
30+
(when (not (and linewise? (= startline endline)))
31+
(when remove-prev?
32+
(table.remove targets))
33+
(set prev-range range)
34+
(set prev-line-range line-range)
35+
(var endline* endline)
36+
(var endcol* endcol)
37+
(when (= endcol 0) ; exclusive
38+
; Go to the end of the previous line.
39+
(set endline* (- endline 1))
40+
(set endcol* (length (vim.fn.getline endline)))) ; (getline 1-indexed)
41+
(table.insert targets ; (0,0) -> (1,1)
42+
{:pos [(+ startline 1) (+ startcol 1)]
43+
; `endcol` is exclusive, but we want to put the
44+
; inline labels after it, so still +1.
45+
:endpos [(+ endline* 1) (+ endcol* 1)]}))))
46+
(when (> (length targets) 0)
47+
targets))
48+
49+
50+
(fn get-targets []
51+
(local (nodes err) (get-nodes))
52+
(if (not nodes) (values nil err)
53+
(nodes->targets nodes)))
54+
55+
56+
(fn select-range [target]
57+
; Enter Visual mode.
58+
(local mode (vim.fn.mode true))
59+
(when (mode:match "no?")
60+
(vim.cmd (.. "normal! " (or (mode:match "[V\22]") "v"))))
61+
; Do the rest without leaving Visual mode midway, so that leap-remote
62+
; can keep working.
63+
; Move the cursor to the start of the Visual area if needed.
64+
(when (or (not= (vim.fn.line "v") (vim.fn.line "."))
65+
(not= (vim.fn.col "v") (vim.fn.col ".")))
66+
(vim.cmd "normal! o"))
67+
(vim.fn.cursor (unpack target.pos))
68+
(vim.cmd "normal! o")
69+
(local (endline endcol) (unpack target.endpos))
70+
(vim.fn.cursor endline (- endcol 1))
71+
; Move to the start. This might be more intuitive for incremental
72+
; selection, when the whole range is not visible - nodes are usually
73+
; harder to identify at their end.
74+
(vim.cmd "normal! o"))
75+
76+
77+
(local ns (api.nvim_create_namespace ""))
78+
79+
(fn clear-fill []
80+
(api.nvim_buf_clear_namespace 0 ns 0 -1))
81+
82+
; Fill the gap left by the cursor (which is down on the command line).
83+
; Note: redrawing the cursor with nvim__redraw() is not a satisfying
84+
; solution, since the cursor might still appear in a wrong place
85+
; (thanks to inline labels).
86+
(fn fill-cursor-pos [targets start-idx]
87+
(clear-fill)
88+
(let [[line col] [(vim.fn.line ".") (vim.fn.col ".")]
89+
line-str (vim.fn.getline line)
90+
ch-at-curpos (vim.fn.strpart line-str (- col 1) 1 true)
91+
; On an empty line, add space.
92+
text (if (= ch-at-curpos "") " " ch-at-curpos)]
93+
; Problem: If there is an inline label for the same position, this
94+
; extmark will not be shifted.
95+
(local conflict? (case (. targets start-idx) ; the first labeled node
96+
{:pos [line* col*]} (and (= line* line) (= col* col))))
97+
; Solution (hack): Shift by the number of labels on the given line.
98+
; Note: Getting the cursor's screenpos would not work, as it has not
99+
; moved yet.
100+
; TODO: What if there are other inline extmarks, besides our ones?
101+
(var shift 1)
102+
(when conflict?
103+
(var loop? true)
104+
(var idx (+ start_idx 1))
105+
(while loop?
106+
(case (. targets idx)
107+
nil (set loop? false)
108+
{:pos [line* _]} (if (= line* line)
109+
(do (set shift (+ shift 1))
110+
(set idx (+ idx 1)))
111+
(set loop? false)))))
112+
(api.nvim_buf_set_extmark 0 ns (- line 1) (- col 1)
113+
{:virt_text [[text :Visual]]
114+
:virt_text_pos "overlay"
115+
:virt_text_win_col (when conflict? (+ col shift -1))
116+
:hl_mode "combine"}))
117+
; Continue with the native function body.
118+
true)
119+
120+
121+
(fn select [kwargs]
122+
(local kwargs (or kwargs {}))
123+
(local leap (require "leap"))
124+
(local op-mode? (: (vim.fn.mode true) :match "o"))
125+
(local inc-select? (not op-mode?))
126+
; Add `;` and `,` as traversal keys.
127+
(local sk (vim.deepcopy leap.opts.special_keys))
128+
(set sk.next_target (vim.fn.flatten
129+
(vim.list_extend [";"] [sk.next_target])))
130+
(set sk.prev_target (vim.fn.flatten
131+
(vim.list_extend [","] [sk.prev_target])))
132+
(leap.leap {:target_windows [(api.nvim_get_current_win)]
133+
:targets get-targets
134+
:action select-range
135+
:traversal inc-select? ; allow traversal for the custom action
136+
:opts (vim.tbl_extend :keep
137+
(or kwargs.opts {})
138+
{:labels (when inc-select? "") ; force autojump
139+
:on_beacons (when inc-select? fill-cursor-pos)
140+
:virt_text_pos "inline"
141+
:special_keys sk})})
142+
(when inc-select?
143+
(clear-fill)))
144+
145+
146+
{: select}

lua/leap/beacons.lua

+17-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lua/leap/main.lua

+6-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)