|
| 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} |
0 commit comments