├── .gitignore ├── LICENSE ├── README.md ├── doc ├── syntax-tree-surfer.txt └── tags └── lua └── syntax-tree-surfer └── init.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *.vim 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ziontee113 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌳 syntax-tree-surfer 🌳🌊 2 | 3 | ### Syntax Tree Surfer is a plugin for Neovim that helps you surf through your document and move elements around using the nvim-treesitter API. 4 | 5 | ![tree surfing cover](https://user-images.githubusercontent.com/102876811/163170119-89369c35-a061-4058-aaeb-1706ea6fa4cf.jpg) 6 | 7 | ## Table of Contents 8 | 9 | 1. [Version 1.0 Functionalities](#version-10-functionalities) 10 | 1. [How do I install?](#how-do-i-install) 11 | 1. [Version 1.1 Update](#version-11-update) 12 | 1. [Version 2.0 Beta Update](#version-20-beta-update) 13 | 1. [Version 2.2 Update](#version-22-update) 14 | 15 | # Version 1.0 Functionalities 16 | 17 | ### **Navigate** around your document based on Treesitter's abstract Syntax Tree. Step into, step out, step over, step back. 18 | 19 | https://user-images.githubusercontent.com/102876811/163170843-a7c9f1a1-4ffb-4a39-9636-fc81521bd9b5.mp4 20 | 21 | --- 22 | 23 | ### **Move / Swap** elements around based on your visual selection 24 | 25 | 26 | 27 | https://user-images.githubusercontent.com/102876811/163171686-4ad49b7a-9fd3-41d5-a2c2-deae1bb41c3d.mp4 28 | 29 | ### **Swap in Normal Mode** - Now supports Dot (.) Repeat 30 | 31 | https://user-images.githubusercontent.com/102876811/174811583-52b7beb0-853f-4ac9-9498-eb718ce626d9.mp4 32 | 33 | 34 | 35 | # How do I install? 36 | 37 | #### Use your favorite Plugin Manager with the link [ziontee113/syntax-tree-surfer](ziontee113/syntax-tree-surfer) 38 | 39 | For Packer: 40 | 41 | ```lua 42 | use "ziontee113/syntax-tree-surfer" 43 | ``` 44 | 45 | # How do I set things up? 46 | 47 | ### Here's my suggestion: 48 | 49 | ```lua 50 | -- Syntax Tree Surfer 51 | local opts = {noremap = true, silent = true} 52 | 53 | -- Normal Mode Swapping: 54 | -- Swap The Master Node relative to the cursor with it's siblings, Dot Repeatable 55 | vim.keymap.set("n", "vU", function() 56 | vim.opt.opfunc = "v:lua.STSSwapUpNormal_Dot" 57 | return "g@l" 58 | end, { silent = true, expr = true }) 59 | vim.keymap.set("n", "vD", function() 60 | vim.opt.opfunc = "v:lua.STSSwapDownNormal_Dot" 61 | return "g@l" 62 | end, { silent = true, expr = true }) 63 | 64 | -- Swap Current Node at the Cursor with it's siblings, Dot Repeatable 65 | vim.keymap.set("n", "vd", function() 66 | vim.opt.opfunc = "v:lua.STSSwapCurrentNodeNextNormal_Dot" 67 | return "g@l" 68 | end, { silent = true, expr = true }) 69 | vim.keymap.set("n", "vu", function() 70 | vim.opt.opfunc = "v:lua.STSSwapCurrentNodePrevNormal_Dot" 71 | return "g@l" 72 | end, { silent = true, expr = true }) 73 | 74 | --> If the mappings above don't work, use these instead (no dot repeatable) 75 | -- vim.keymap.set("n", "vd", 'STSSwapCurrentNodeNextNormal', opts) 76 | -- vim.keymap.set("n", "vu", 'STSSwapCurrentNodePrevNormal', opts) 77 | -- vim.keymap.set("n", "vD", 'STSSwapDownNormal', opts) 78 | -- vim.keymap.set("n", "vU", 'STSSwapUpNormal', opts) 79 | 80 | -- Visual Selection from Normal Mode 81 | vim.keymap.set("n", "vx", 'STSSelectMasterNode', opts) 82 | vim.keymap.set("n", "vn", 'STSSelectCurrentNode', opts) 83 | 84 | -- Select Nodes in Visual Mode 85 | vim.keymap.set("x", "J", 'STSSelectNextSiblingNode', opts) 86 | vim.keymap.set("x", "K", 'STSSelectPrevSiblingNode', opts) 87 | vim.keymap.set("x", "H", 'STSSelectParentNode', opts) 88 | vim.keymap.set("x", "L", 'STSSelectChildNode', opts) 89 | 90 | -- Swapping Nodes in Visual Mode 91 | vim.keymap.set("x", "", 'STSSwapNextVisual', opts) 92 | vim.keymap.set("x", "", 'STSSwapPrevVisual', opts) 93 | ``` 94 | 95 | # Now let's start Tree Surfing! 🌲💦 96 | 97 | ### Version 1.1 update 98 | 99 | This feature will help you save some keystrokes & brain power when you want to create some code at the top level node of your current cursor position. 100 | 101 | ```lua 102 | lua require("syntax-tree-surfer").go_to_top_node_and_execute_commands(false, { "normal! O", "normal! O", "startinsert" }) 103 | ``` 104 | 105 | The .go_to_top_node_and_execute_commands() method takes 2 arguments: 106 | 107 | 1. boolean: if false then it will jump to the beginning of the node, if true it jumps to the end. 108 | 109 | 1. lua table: a table that contains strings, each string is a vim command example: { "normal! O", "normal! O", "startinsert" } 110 | 111 | --- 112 | 113 | # Version 2.0 Beta Update 114 | 115 | ### Targeted Jump with Virtual Text 116 | 117 | https://user-images.githubusercontent.com/102876811/169820839-5ec66bd9-bf14-49f6-8e5a-3078b8ec43c4.mp4 118 | 119 | ### Filtered Jump through user-defined node types 120 | 121 | https://user-images.githubusercontent.com/102876811/169820922-b1eefa5e-6ed9-4ebd-95d1-f3f35e0388da.mp4 122 | 123 | ### These are experimental features and I wish to expand them even further. If you have any suggestions, please feel free to let me know 😊 124 | 125 | Example mappings for Version 2.0 Beta functionalities: 126 | 127 | ```lua 128 | -- Syntax Tree Surfer V2 Mappings 129 | -- Targeted Jump with virtual_text 130 | local sts = require("syntax-tree-surfer") 131 | vim.keymap.set("n", "gv", function() -- only jump to variable_declarations 132 | sts.targeted_jump({ "variable_declaration" }) 133 | end, opts) 134 | vim.keymap.set("n", "gfu", function() -- only jump to functions 135 | sts.targeted_jump({ "function", "arrrow_function", "function_definition" }) 136 | --> In this example, the Lua language schema uses "function", 137 | -- when the Python language uses "function_definition" 138 | -- we include both, so this keymap will work on both languages 139 | end, opts) 140 | vim.keymap.set("n", "gif", function() -- only jump to if_statements 141 | sts.targeted_jump({ "if_statement" }) 142 | end, opts) 143 | vim.keymap.set("n", "gfo", function() -- only jump to for_statements 144 | sts.targeted_jump({ "for_statement" }) 145 | end, opts) 146 | vim.keymap.set("n", "gj", function() -- jump to all that you specify 147 | sts.targeted_jump({ 148 | "function", 149 | "if_statement", 150 | "else_clause", 151 | "else_statement", 152 | "elseif_statement", 153 | "for_statement", 154 | "while_statement", 155 | "switch_statement", 156 | }) 157 | end, opts) 158 | 159 | ------------------------------- 160 | -- filtered_jump -- 161 | -- "default" means that you jump to the default_desired_types or your lastest jump types 162 | vim.keymap.set("n", "", function() 163 | sts.filtered_jump("default", true) --> true means jump forward 164 | end, opts) 165 | vim.keymap.set("n", "", function() 166 | sts.filtered_jump("default", false) --> false means jump backwards 167 | end, opts) 168 | 169 | -- non-default jump --> custom desired_types 170 | vim.keymap.set("n", "your_keymap", function() 171 | sts.filtered_jump({ 172 | "if_statement", 173 | "else_clause", 174 | "else_statement", 175 | }, true) --> true means jump forward 176 | end, opts) 177 | vim.keymap.set("n", "your_keymap", function() 178 | sts.filtered_jump({ 179 | "if_statement", 180 | "else_clause", 181 | "else_statement", 182 | }, false) --> false means jump backwards 183 | end, opts) 184 | 185 | ------------------------------- 186 | -- jump with limited targets -- 187 | -- jump to sibling nodes only 188 | vim.keymap.set("n", "-", function() 189 | sts.filtered_jump({ 190 | "if_statement", 191 | "else_clause", 192 | "else_statement", 193 | }, false, { destination = "siblings" }) 194 | end, opts) 195 | vim.keymap.set("n", "=", function() 196 | sts.filtered_jump({ "if_statement", "else_clause", "else_statement" }, true, { destination = "siblings" }) 197 | end, opts) 198 | 199 | -- jump to parent or child nodes only 200 | vim.keymap.set("n", "_", function() 201 | sts.filtered_jump({ 202 | "if_statement", 203 | "else_clause", 204 | "else_statement", 205 | }, false, { destination = "parent" }) 206 | end, opts) 207 | vim.keymap.set("n", "+", function() 208 | sts.filtered_jump({ 209 | "if_statement", 210 | "else_clause", 211 | "else_statement", 212 | }, true, { destination = "children" }) 213 | end, opts) 214 | 215 | -- Setup Function example: 216 | -- These are the default options: 217 | require("syntax-tree-surfer").setup({ 218 | highlight_group = "STS_highlight", 219 | disable_no_instance_found_report = false, 220 | default_desired_types = { 221 | "function", 222 | "arrow_function", 223 | "function_definition", 224 | "if_statement", 225 | "else_clause", 226 | "else_statement", 227 | "elseif_statement", 228 | "for_statement", 229 | "while_statement", 230 | "switch_statement", 231 | }, 232 | left_hand_side = "fdsawervcxqtzb", 233 | right_hand_side = "jkl;oiu.,mpy/n", 234 | icon_dictionary = { 235 | ["if_statement"] = "", 236 | ["else_clause"] = "", 237 | ["else_statement"] = "", 238 | ["elseif_statement"] = "", 239 | ["for_statement"] = "ﭜ", 240 | ["while_statement"] = "ﯩ", 241 | ["switch_statement"] = "ﳟ", 242 | ["function"] = "", 243 | ["function_definition"] = "", 244 | ["variable_declaration"] = "", 245 | }, 246 | }) 247 | ``` 248 | 249 | ### Because every languages have different schemas and node-types, you can check the node-types that you're interested in with https://github.com/nvim-treesitter/playground 250 | 251 | #### You can also do a quick check using the command :STSPrintNodesAtCursor 252 | 253 | 254 | # Version 2.2 Update 255 | 256 | ### Hold and swap nodes 257 | https://user-images.githubusercontent.com/8104435/225992362-4e82d677-2ff5-463a-a910-6a6bdbf4fc9c.mp4 258 | 259 | This feature allows marking a node and then swapping it with another node. 260 | 261 | Example mapping: 262 | 263 | ```lua 264 | -- Holds a node, or swaps the held node 265 | vim.keymap.set("n", "gnh", "STSSwapOrHold", opts) 266 | -- Same for visual 267 | vim.keymap.set("x", "gnh", "STSSwapOrHoldVisual", opts) 268 | ``` 269 | 270 | The lower-level functionality can be accessed via: 271 | ```lua 272 | require("syntax-tree-surfer").hold_or_swap(true) -- param is_visual boolean 273 | require("syntax-tree-surfer").clear_held_node() 274 | ``` 275 | note that `STSSwapOrHoldVisual` will clear the visual selection, but `hold_or_swap(true)` will not. 276 | 277 | # Special Thanks To: 278 | ### Dr. David A. Kunz for creating [Let's create a Neovim plugin using Treesitter and Lua](https://www.youtube.com/watch?v=dPQfsASHNkg) 279 | ### NVIM Treesitter Team - https://github.com/nvim-treesitter/nvim-treesitter 280 | ### @lmburns for [#9](https://github.com/ziontee113/syntax-tree-surfer/pull/9) 281 | ### @spiderforrest for [#14](https://github.com/ziontee113/syntax-tree-surfer/pull/14) 282 | -------------------------------------------------------------------------------- /doc/syntax-tree-surfer.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziontee113/syntax-tree-surfer/732ea6d0f868bcccd2f526be73afa46997d5a2fb/doc/syntax-tree-surfer.txt -------------------------------------------------------------------------------- /doc/tags: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziontee113/syntax-tree-surfer/732ea6d0f868bcccd2f526be73afa46997d5a2fb/doc/tags -------------------------------------------------------------------------------- /lua/syntax-tree-surfer/init.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: missing-parameter, empty-block 2 | 3 | local ts_utils = require("nvim-treesitter.ts_utils") 4 | 5 | local M = {} 6 | 7 | local function get_node_at_cursor() 8 | local r, c = unpack(vim.api.nvim_win_get_cursor(0)) 9 | vim.treesitter.get_parser(0):parse({ r - 1, c, r - 1, c }) 10 | return vim.treesitter.get_node() 11 | end 12 | 13 | local function find_range_from_2nodes(nodeA, nodeB) --{{{ 14 | local start_row_A, start_col_A, end_row_A, end_col_A = nodeA:range() 15 | local start_row_B, start_col_B, end_row_B, end_col_B = nodeB:range() 16 | 17 | local true_range = {} 18 | 19 | if start_row_A == start_row_B then 20 | if start_col_A < start_col_B then 21 | table.insert(true_range, start_row_A) 22 | table.insert(true_range, start_col_A) 23 | else 24 | table.insert(true_range, start_row_B) 25 | table.insert(true_range, start_col_B) 26 | end 27 | end 28 | 29 | if start_row_A < start_row_B then 30 | table.insert(true_range, start_row_A) 31 | table.insert(true_range, start_col_A) 32 | elseif start_row_A > start_row_B then 33 | table.insert(true_range, start_row_B) 34 | table.insert(true_range, start_col_B) 35 | end 36 | 37 | if end_row_A == end_row_B then 38 | if end_col_A > end_col_B then 39 | table.insert(true_range, end_row_A) 40 | table.insert(true_range, end_col_A) 41 | else 42 | table.insert(true_range, end_row_B) 43 | table.insert(true_range, end_col_B) 44 | end 45 | end 46 | if end_row_A > end_row_B then 47 | table.insert(true_range, end_row_A) 48 | table.insert(true_range, end_col_A) 49 | elseif end_row_A < end_row_B then 50 | table.insert(true_range, end_row_B) 51 | table.insert(true_range, end_col_B) 52 | end 53 | 54 | return true_range 55 | end --}}} 56 | 57 | function M.update_selection(buf, node, selection_mode) -- rip from the old ts_utils{{{ 58 | selection_mode = selection_mode or "charwise" 59 | local start_row, start_col, end_row, end_col = ts_utils.get_vim_range({ vim.treesitter.get_node_range(node) }, buf) 60 | 61 | vim.fn.setpos(".", { buf, start_row, start_col, 0 }) 62 | 63 | -- Start visual selection in appropriate mode 64 | local v_table = { charwise = "v", linewise = "V", blockwise = "" } 65 | ---- Call to `nvim_replace_termcodes()` is needed for sending appropriate 66 | ---- command to enter blockwise mode 67 | local mode_string = vim.api.nvim_replace_termcodes(v_table[selection_mode] or selection_mode, true, true, true) 68 | vim.cmd("normal! " .. mode_string) 69 | vim.fn.setpos(".", { buf, end_row, end_col, 0 }) 70 | end --}}} 71 | 72 | local get_visual_node = function() --{{{ 73 | local node = get_node_at_cursor() -- declare node and bufnr 74 | 75 | if node == nil then -- prevent errors 76 | return 77 | end 78 | 79 | local nodeA = node 80 | vim.cmd("normal! o") 81 | local nodeB = get_node_at_cursor() 82 | vim.cmd("normal! o") 83 | local root = ts_utils.get_root_for_node(node) 84 | 85 | if nodeA:id() ~= nodeB:id() then --> get the true node 86 | local true_range = find_range_from_2nodes(nodeA, nodeB) 87 | local parent = nodeA:parent() 88 | 89 | if not parent then 90 | return 91 | end 92 | if not parent.range then 93 | return 94 | end 95 | 96 | local start_row_P, start_col_P, end_row_P, end_col_P = parent:range() 97 | 98 | while 99 | start_row_P ~= true_range[1] 100 | or start_col_P ~= true_range[2] 101 | or end_row_P ~= true_range[3] 102 | or end_col_P ~= true_range[4] 103 | do 104 | if parent:parent() == nil then 105 | break 106 | end 107 | parent = parent:parent() 108 | start_row_P, start_col_P, end_row_P, end_col_P = parent:range() 109 | end 110 | 111 | node = parent 112 | end 113 | 114 | if node == root then -- catch some edge cases 115 | node = nodeA 116 | end 117 | 118 | local parent = node:parent() --> if parent only has 1 child, move up the tree 119 | while parent ~= nil and parent:named_child_count() == 1 do 120 | node = parent 121 | parent = node:parent() 122 | end 123 | 124 | return node 125 | end --}}} 126 | 127 | M.surf = function(direction, mode, move) --{{{ 128 | local node = get_node_at_cursor() -- declare node and bufnr 129 | local bufnr = vim.api.nvim_get_current_buf() 130 | 131 | if node == nil then -- prevent errors 132 | return 133 | end 134 | 135 | if mode == "visual" then -- {{{ 136 | node = get_visual_node() 137 | else 138 | local parent = node:parent() 139 | while parent ~= nil and parent:named_child_count() == 1 do 140 | node = parent 141 | parent = node:parent() 142 | end 143 | end --}}} 144 | 145 | if not node then 146 | return 147 | end 148 | 149 | local target --> setting the target, depending on the direction 150 | if direction == "parent" then 151 | target = node:parent() 152 | elseif direction == "child" and node ~= nil then 153 | while node ~= nil do 154 | if node:named_child_count() >= 2 then 155 | target = node:named_child(0) 156 | break 157 | end 158 | node = node:named_child(0) 159 | end 160 | else 161 | target = node:next_named_sibling() -- naively look for next or prev sibling based on direction 162 | if direction == "prev" then 163 | target = node:prev_named_sibling() 164 | end 165 | 166 | while target ~= nil and target:type() == "comment" do -- skip over the comments because how comments are treated in Treesitter 167 | if direction == "prev" then 168 | target = target:prev_named_sibling() 169 | else 170 | target = target:next_named_sibling() 171 | end 172 | end 173 | end 174 | 175 | if target ~= nil then 176 | if move == true then 177 | ts_utils.swap_nodes(node, target, bufnr, true) 178 | 179 | if mode == "visual" then 180 | target = get_node_at_cursor() 181 | M.update_selection(bufnr, target) 182 | M.update_selection(bufnr, target) 183 | end 184 | else 185 | M.update_selection(bufnr, target) --> make the selection 186 | if mode == "visual" then 187 | M.update_selection(bufnr, target) 188 | end 189 | end 190 | end 191 | end --}}} 192 | 193 | M.select_current_node = function() --{{{ 194 | local node = get_node_at_cursor() 195 | local bufnr = vim.api.nvim_get_current_buf() 196 | 197 | if node ~= nil then 198 | M.update_selection(bufnr, node) 199 | end 200 | end --}}} 201 | 202 | --- 203 | 204 | M.jump_to_current_node = function(start_or_end) 205 | local node = get_node_at_cursor() 206 | ts_utils.goto_node(node, start_or_end, true) 207 | end 208 | 209 | --- 210 | 211 | local function get_master_node(block_check) --{{{ 212 | local node = get_node_at_cursor() 213 | if node == nil then 214 | error("No Treesitter parser found") 215 | end 216 | 217 | local root = ts_utils.get_root_for_node(node) 218 | 219 | local start_row = node:start() 220 | local parent = node:parent() 221 | 222 | while parent ~= nil and parent ~= root and parent:start() == start_row do 223 | if block_check and parent:type() == "block" then 224 | break 225 | end 226 | 227 | node = parent 228 | parent = node:parent() 229 | -- print(node:type()) 230 | end 231 | 232 | return node 233 | end --}}} 234 | 235 | M.select = function() --{{{ 236 | local node = get_master_node() 237 | local bufnr = vim.api.nvim_get_current_buf() 238 | 239 | M.update_selection(bufnr, node) 240 | end --}}} 241 | 242 | M.move = function(mode, up) --{{{ 243 | local node = get_master_node(true) 244 | local bufnr = vim.api.nvim_get_current_buf() 245 | 246 | local target 247 | if up == true then 248 | target = node:prev_named_sibling() 249 | else 250 | target = node:next_named_sibling() 251 | end 252 | 253 | if target == nil then 254 | return 255 | end 256 | 257 | while target:type() == "comment" do 258 | if up == true then 259 | target = target:prev_named_sibling() 260 | else 261 | target = target:next_named_sibling() 262 | end 263 | end 264 | 265 | if target ~= nil then 266 | ts_utils.swap_nodes(node, target, bufnr, true) 267 | 268 | if mode == "v" then 269 | target = get_node_at_cursor() 270 | M.update_selection(bufnr, target) 271 | M.update_selection(bufnr, target) 272 | end 273 | end 274 | end --}}} 275 | 276 | --! Create User Commands for 1.0 functionalities !{{{ 277 | 278 | -- Swap in Normal Mode 279 | vim.api.nvim_create_user_command("STSSwapUpNormal", function() 280 | M.move("n", true) 281 | end, {}) 282 | vim.api.nvim_create_user_command("STSSwapDownNormal", function() 283 | M.move("n", false) 284 | end, {}) 285 | vim.api.nvim_create_user_command("STSSwapCurrentNodePrevNormal", function() 286 | M.surf("prev", "normal", true) 287 | end, {}) 288 | vim.api.nvim_create_user_command("STSSwapCurrentNodeNextNormal", function() 289 | M.surf("next", "normal", true) 290 | end, {}) 291 | 292 | -- Select Node from Normal Mode 293 | vim.api.nvim_create_user_command("STSSelectCurrentNode", function() 294 | M.select_current_node() 295 | end, {}) 296 | vim.api.nvim_create_user_command("STSSelectMasterNode", function() 297 | M.select() 298 | end, {}) 299 | 300 | -- Jump to Node in Normal Mode 301 | vim.api.nvim_create_user_command("STSJumpToStartOfCurrentNode", function() 302 | M.jump_to_current_node(false) 303 | end, {}) 304 | vim.api.nvim_create_user_command("STSJumpToEndOfCurrentNode", function() 305 | M.jump_to_current_node(true) 306 | end, {}) 307 | 308 | -- Select Node from Visual Mode 309 | vim.api.nvim_create_user_command("STSSelectParentNode", function() 310 | M.surf("parent", "visual") 311 | end, {}) 312 | vim.api.nvim_create_user_command("STSSelectChildNode", function() 313 | M.surf("child", "visual") 314 | end, {}) 315 | vim.api.nvim_create_user_command("STSSelectPrevSiblingNode", function() 316 | M.surf("prev", "visual") 317 | end, {}) 318 | vim.api.nvim_create_user_command("STSSelectNextSiblingNode", function() 319 | M.surf("next", "visual") 320 | end, {}) 321 | 322 | -- Swap in Visual Mode 323 | vim.api.nvim_create_user_command("STSSwapNextVisual", function() 324 | M.surf("next", "visual", true) 325 | end, {}) 326 | vim.api.nvim_create_user_command("STSSwapPrevVisual", function() 327 | M.surf("prev", "visual", true) 328 | end, {}) --}}} 329 | 330 | -- Global Variables for Normal Swap Dot Repeat{{{ 331 | _G.STSSwapCurrentNodePrevNormal_Dot = function() 332 | vim.cmd("STSSwapCurrentNodePrevNormal") 333 | end 334 | _G.STSSwapCurrentNodeNextNormal_Dot = function() 335 | vim.cmd("STSSwapCurrentNodeNextNormal") 336 | end 337 | _G.STSSwapUpNormal_Dot = function() 338 | vim.cmd("STSSwapUpNormal") 339 | end 340 | _G.STSSwapDownNormal_Dot = function() 341 | vim.cmd("STSSwapDownNormal") 342 | end --}}} 343 | 344 | --- version 1.1 345 | 346 | local function get_top_node() --{{{ 347 | local node = get_node_at_cursor() 348 | if node == nil then 349 | error("No Treesitter parser found") 350 | end 351 | 352 | local root = ts_utils.get_root_for_node(node) 353 | 354 | local parent = node:parent() 355 | 356 | while parent ~= nil and parent ~= root do 357 | node = parent 358 | parent = node:parent() 359 | end 360 | 361 | return node 362 | end --}}} 363 | 364 | local function go_to_top_node(go_to_end) --{{{ 365 | local node = get_top_node() 366 | ts_utils.goto_node(node, go_to_end) 367 | end --}}} 368 | 369 | M.go_to_top_node_and_execute_commands = function(go_to_end, list_of_commands) --{{{ 370 | go_to_top_node(go_to_end) 371 | 372 | -- I want to create a function at the top level 373 | vim.schedule(function() 374 | for _, command in ipairs(list_of_commands) do 375 | if type(command) == "string" then 376 | vim.cmd(command) 377 | else 378 | command() 379 | end 380 | end 381 | end) 382 | end --}}} 383 | 384 | M.go_to_node_and_execute_commands = function(node, go_to_end, list_of_commands) --{{{ 385 | ts_utils.goto_node(node, go_to_end) 386 | 387 | -- I want to create a function at the top level 388 | vim.schedule(function() 389 | for _, command in pairs(list_of_commands) do 390 | command() 391 | end 392 | end) 393 | end --}}} 394 | 395 | M.get_master_node = get_master_node 396 | 397 | -- version 2.0 Beta -- 398 | 399 | -- Imports & Aliases{{{ 400 | M.opts = { 401 | disable_no_instance_found_report = false, 402 | highlight_group = "STS_highlight", 403 | } 404 | 405 | vim.cmd(":highlight STS_highlight guifg=#00F1F5") 406 | 407 | local api = vim.api 408 | local ns = api.nvim_create_namespace("tree_testing_ns") 409 | 410 | local current_desired_types = { 411 | "function", 412 | "if_statement", 413 | "else_clause", 414 | "else_statement", 415 | "elseif_statement", 416 | "for_statement", 417 | "while_statement", 418 | "switch_statement", 419 | } -- default desired types }}} 420 | 421 | -- Dictionary{{{ 422 | M.opts.icon_dictionary = { 423 | ["if_statement"] = "", 424 | ["else_clause"] = "", 425 | ["else_statement"] = "", 426 | ["elseif_statement"] = "", 427 | ["for_statement"] = "ﭜ", 428 | ["while_statement"] = "ﯩ", 429 | ["switch_statement"] = "ﳟ", 430 | ["function"] = "", 431 | ["variable_declaration"] = "", 432 | ["comment"] = "", 433 | } 434 | 435 | -- Possible keymaps for jumping 436 | M.opts.left_hand_side = "fdsawervcxqtzb" 437 | M.opts.left_hand_side = vim.split(M.opts.left_hand_side, "") 438 | M.opts.right_hand_side = "jkl;oiu.,mpy/n" 439 | M.opts.right_hand_side = vim.split(M.opts.right_hand_side, "") --}}} 440 | 441 | -- Utils (Getters) 442 | local function recursive_child_iter(node, table_to_insert, desired_types) -- {{{ 443 | if node:iter_children() then 444 | for child in node:iter_children() do 445 | if desired_types then 446 | if vim.tbl_contains(desired_types, child:type()) then 447 | table.insert(table_to_insert, child) 448 | end 449 | else 450 | table.insert(table_to_insert, child) 451 | end 452 | 453 | recursive_child_iter(child, table_to_insert, desired_types) 454 | end 455 | end 456 | end --}}} 457 | local function filter_children_recursively(node, desired_types) --{{{ 458 | local children = {} 459 | 460 | recursive_child_iter(node, children, desired_types) 461 | 462 | return children 463 | end --}}} 464 | 465 | local function get_nodes_in_array() --{{{ 466 | local ts = vim.treesitter 467 | local current_buffer = vim.api.nvim_get_current_buf() 468 | 469 | -- Yanked from https://github.com/nvim-treesitter/nvim-treesitter/blob/32e364ea3c99aafcce2ce735fe091618f623d889/lua/nvim-treesitter/parsers.lua#L4-L21 470 | local filetype_to_parsername = { 471 | arduino = "cpp", 472 | javascriptreact = "javascript", 473 | ecma = "javascript", 474 | jsx = "javascript", 475 | PKGBUILD = "bash", 476 | html_tags = "html", 477 | typescriptreact = "tsx", 478 | ["typescript.tsx"] = "tsx", 479 | terraform = "hcl", 480 | ["html.handlebars"] = "glimmer", 481 | systemverilog = "verilog", 482 | cls = "latex", 483 | sty = "latex", 484 | OpenFOAM = "foam", 485 | pandoc = "markdown", 486 | rmd = "markdown", 487 | cs = "c_sharp", 488 | } 489 | 490 | local ok, parser = pcall(ts.get_parser, 0) 491 | if not ok then 492 | local cur_buf_filetype = vim.bo[current_buffer].ft 493 | parser = ts.get_parser(0, filetype_to_parsername[cur_buf_filetype]) 494 | end 495 | 496 | local trees = parser:parse() 497 | local root = trees[1]:root() 498 | 499 | local nodes = {} 500 | 501 | recursive_child_iter(root, nodes) 502 | 503 | return nodes 504 | end --}}} 505 | local function get_desired_nodes(nodes, desired_types) --{{{ 506 | -- get current cursor position 507 | local return_nodes = {} 508 | 509 | -- loop through nodes 510 | for i = 1, #nodes do 511 | local node = nodes[i] 512 | local node_type = node:type() 513 | local start_row, start_col, end_row, end_col = node:range() 514 | 515 | -- if node_type is in desired_types, add to return_nodes 516 | if vim.tbl_contains(desired_types, node_type) then 517 | table.insert(return_nodes, node) 518 | end 519 | end 520 | 521 | return return_nodes 522 | end --}}} 523 | 524 | local function filter_sibling_nodes(node, desired_types) --{{{ 525 | local current_node_id = node:id() 526 | local parent = node:parent() 527 | local return_nodes = {} 528 | 529 | for child in parent:iter_children() do 530 | if child:id() ~= current_node_id then 531 | local node_type = child:type() 532 | 533 | if vim.tbl_contains(desired_types, node_type) then 534 | table.insert(return_nodes, child) 535 | end 536 | end 537 | end 538 | 539 | return return_nodes 540 | end --}}} 541 | local function filter_nearest_parent(node, desired_types) --{{{ 542 | if node:parent() then 543 | local parent = node:parent() 544 | local parent_type = parent:type() 545 | 546 | if vim.tbl_contains(desired_types, parent_type) then 547 | return parent 548 | else 549 | return filter_nearest_parent(parent, desired_types) 550 | end 551 | else 552 | return nil 553 | end 554 | end --}}} 555 | 556 | local function get_parent_nodes(node, desired_types) --{{{ 557 | local parents = {} 558 | 559 | while node:parent() do 560 | node = node:parent() 561 | local node_type = node:type() 562 | 563 | if vim.tbl_contains(desired_types, node_type) then 564 | table.insert(parents, node) 565 | end 566 | end 567 | 568 | return parents 569 | end --}}} 570 | local function set_extmark_then_delete_it(start_row, start_col, contents, color_group, timeout) --{{{ 571 | -- if start_col <= 0 then 572 | -- start_col = 1 573 | -- end 574 | 575 | if not contents then 576 | contents = "" 577 | end 578 | 579 | local extmark_id = api.nvim_buf_set_extmark(0, ns, start_row, start_col - 0, { 580 | virt_text = { { contents, color_group } }, 581 | virt_text_pos = "overlay", 582 | }) 583 | 584 | local timer = vim.loop.new_timer() 585 | timer:start( 586 | timeout, 587 | timeout, 588 | vim.schedule_wrap(function() 589 | api.nvim_buf_del_extmark(0, ns, extmark_id) 590 | end) 591 | ) 592 | 593 | return extmark_id 594 | end --}}} 595 | 596 | local function has_value(tab, val) --{{{ 597 | for index, value in ipairs(tab) do 598 | if value == val then 599 | return true 600 | end 601 | end 602 | 603 | return false 604 | end --}}} 605 | 606 | -- Functions to Execute -- 607 | local function print_types(desired_types) -- {{{ 608 | vim.cmd("m'") 609 | 610 | current_desired_types = desired_types 611 | 612 | local nodes = get_nodes_in_array() 613 | 614 | local current_window = api.nvim_get_current_win() 615 | local current_line = vim.api.nvim_win_get_cursor(current_window)[1] 616 | 617 | local nodes_before_cursor = {} 618 | local nodes_after_cursor = {} 619 | 620 | local hash_table = {} 621 | 622 | for _, node in ipairs(nodes) do 623 | local start_row, start_col, end_row, end_col = node:range() 624 | 625 | if start_row + 1 < current_line then 626 | table.insert(nodes_before_cursor, node) 627 | elseif start_row + 1 > current_line then 628 | table.insert(nodes_after_cursor, node) 629 | end 630 | end 631 | 632 | local color_group = M.opts.highlight_group 633 | 634 | -- loop backwards through nodes_before_cursor 635 | local count = 1 636 | for i = #nodes_before_cursor, 1, -1 do 637 | local node = nodes_before_cursor[i] 638 | local node_type = node:type() 639 | local start_row, start_col = node:range() 640 | 641 | if not M.opts.left_hand_side[count] then 642 | break 643 | end 644 | 645 | if has_value(desired_types, node_type) then 646 | if start_col - 1 < 0 then 647 | start_col = 0 648 | else 649 | -- start_col = start_col - 1 650 | start_col = start_col 651 | end 652 | api.nvim_buf_set_extmark(0, ns, start_row, start_col, { 653 | virt_text = { { M.opts.left_hand_side[count], color_group } }, 654 | virt_text_pos = "overlay", 655 | }) 656 | 657 | api.nvim_buf_set_extmark(0, ns, start_row, -1, { 658 | virt_text = { { " " .. M.opts.left_hand_side[count] .. " <-- " .. node_type, color_group } }, 659 | virt_text_pos = "eol", 660 | }) 661 | 662 | hash_table[M.opts.left_hand_side[count]] = {} 663 | hash_table[M.opts.left_hand_side[count]].start_row = start_row 664 | hash_table[M.opts.left_hand_side[count]].start_col = start_col 665 | 666 | count = count + 1 667 | end 668 | end 669 | 670 | count = 1 671 | for i = 1, #nodes_after_cursor do 672 | local node = nodes_after_cursor[i] 673 | local node_type = node:type() 674 | local start_row, start_col = node:range() 675 | 676 | if not M.opts.right_hand_side[count] then 677 | break 678 | end 679 | 680 | if has_value(desired_types, node_type) then 681 | if start_col - 1 < 0 then 682 | start_col = 0 683 | else 684 | -- start_col = start_col - 1 685 | start_col = start_col 686 | end 687 | api.nvim_buf_set_extmark(0, ns, start_row, start_col, { 688 | virt_text = { { M.opts.right_hand_side[count], color_group } }, 689 | virt_text_pos = "overlay", 690 | }) 691 | 692 | api.nvim_buf_set_extmark(0, ns, start_row, -1, { 693 | virt_text = { { " " .. M.opts.right_hand_side[count] .. " <-- " .. node_type, color_group } }, 694 | virt_text_pos = "eol", 695 | }) 696 | 697 | hash_table[M.opts.right_hand_side[count]] = {} 698 | hash_table[M.opts.right_hand_side[count]].start_row = start_row 699 | hash_table[M.opts.right_hand_side[count]].start_col = start_col 700 | 701 | count = count + 1 702 | end 703 | end 704 | 705 | local key_count = 0 706 | for _, _ in pairs(hash_table) do 707 | key_count = key_count + 1 708 | end 709 | if key_count == 0 then 710 | return 711 | end 712 | 713 | vim.cmd([[redraw]]) 714 | 715 | local ok, keynum = pcall(vim.fn.getchar) 716 | if ok then 717 | local key = string.char(keynum) 718 | if hash_table[key] then 719 | local start_row = hash_table[key].start_row + 1 720 | local start_col = hash_table[key].start_col 721 | 722 | vim.api.nvim_win_set_cursor(current_window, { start_row, start_col }) 723 | end 724 | end 725 | 726 | api.nvim_buf_clear_namespace(0, ns, 0, -1) 727 | end --}}} 728 | local function go_to_next_instance(desired_types, forward, opts) --{{{ 729 | if desired_types == "default" then 730 | desired_types = current_desired_types 731 | end 732 | 733 | -- get nodes to operate on 734 | local nodes = get_nodes_in_array() 735 | 736 | -- get cursor position 737 | local current_window = api.nvim_get_current_win() 738 | local current_line = vim.api.nvim_win_get_cursor(current_window)[1] 739 | 740 | -- set up variables 741 | local previous_closest_node = nil 742 | local next_closest_node = nil 743 | local previous_closest_node_line = nil 744 | local next_closest_node_line = nil 745 | 746 | local previous_closest_node_index = nil 747 | local next_closest_node_index = nil 748 | 749 | if nodes then 750 | -- filter the nodes based on the opts 751 | if opts then 752 | local current_node = get_node_at_cursor(current_window) 753 | 754 | if opts.destination == "parent" then 755 | nodes = get_parent_nodes(current_node, desired_types) 756 | previous_closest_node = nodes[1] 757 | previous_closest_node_index = 1 758 | end 759 | 760 | if opts.destination == "children" then 761 | nodes = filter_children_recursively(current_node, desired_types) 762 | end 763 | 764 | if opts.destination == "siblings" then 765 | nodes = filter_sibling_nodes(current_node, desired_types) 766 | 767 | if #nodes == 0 then 768 | nodes = {} 769 | -- if the current node type is in desired_types, then don't filter 770 | if not vim.tbl_contains(desired_types, current_node:type()) then 771 | previous_closest_node = filter_nearest_parent(current_node, desired_types) 772 | previous_closest_node_index = 1 773 | end 774 | end 775 | end 776 | else 777 | nodes = get_desired_nodes(nodes, desired_types) 778 | end 779 | 780 | -- find closest nodes before & after cursor 781 | for index, node in ipairs(nodes) do 782 | local start_row, start_col, end_row, end_col = node:range() 783 | 784 | -- TODO:: change the logic here 785 | if start_row + 1 < current_line then 786 | if previous_closest_node == nil then 787 | previous_closest_node = node 788 | previous_closest_node_line = start_row 789 | previous_closest_node_index = index 790 | elseif previous_closest_node_line and start_row > previous_closest_node_line then 791 | previous_closest_node = node 792 | previous_closest_node_index = index 793 | end 794 | elseif start_row + 1 > current_line then 795 | if next_closest_node == nil then 796 | next_closest_node = node 797 | next_closest_node_line = start_row 798 | next_closest_node_index = index 799 | elseif next_closest_node_line and start_row < next_closest_node_line then 800 | next_closest_node = node 801 | next_closest_node_index = index 802 | end 803 | end 804 | end 805 | end 806 | 807 | -- depends on forward or not, set cursor to closest node 808 | local cursor_moved = false 809 | if forward then 810 | if next_closest_node then 811 | local start_row, start_col, end_row, end_col = next_closest_node:range() 812 | vim.api.nvim_win_set_cursor(current_window, { start_row + 1, start_col }) 813 | cursor_moved = true 814 | end 815 | else 816 | if previous_closest_node then 817 | local start_row, start_col, end_row, end_col = previous_closest_node:range() 818 | vim.api.nvim_win_set_cursor(current_window, { start_row + 1, start_col }) 819 | cursor_moved = true 820 | end 821 | end 822 | 823 | -- if there is no next instance, print message 824 | if not cursor_moved then 825 | if not M.opts.disable_no_instance_found_report then 826 | if forward then 827 | print("No next instance found") 828 | else 829 | print("No previous instance found") 830 | end 831 | end 832 | else -- if cursor moved 833 | if not opts then 834 | if forward then 835 | while next_closest_node_index + 1 <= #nodes do 836 | local start_row, start_col = nodes[next_closest_node_index + 1]:range() 837 | set_extmark_then_delete_it( 838 | start_row, 839 | start_col, 840 | M.opts.icon_dictionary[nodes[next_closest_node_index + 1]:type()], 841 | M.opts.highlight_group, 842 | 800 843 | ) 844 | next_closest_node_index = next_closest_node_index + 1 845 | end 846 | else 847 | while previous_closest_node_index - 1 >= 1 do 848 | local start_row, start_col = nodes[previous_closest_node_index - 1]:range() 849 | set_extmark_then_delete_it( 850 | start_row, 851 | start_col, 852 | M.opts.icon_dictionary[nodes[previous_closest_node_index - 1]:type()], 853 | M.opts.highlight_group, 854 | 800 855 | ) 856 | previous_closest_node_index = previous_closest_node_index - 1 857 | end 858 | end 859 | end 860 | end 861 | end --}}} 862 | 863 | -- Methods to return {{{ 864 | M.filtered_jump = go_to_next_instance 865 | M.targeted_jump = print_types --}}} 866 | -- Setup Function{{{ 867 | M.setup = function(opts) 868 | if opts then 869 | for key, value in pairs(opts) do 870 | if key == "default_desired_types" then 871 | current_desired_types = value 872 | else 873 | M.opts[key] = value 874 | 875 | if key == "left_hand_side" then 876 | M.opts.left_hand_side = vim.split(value, "") 877 | elseif key == "right_hand_side" then 878 | M.opts.right_hand_side = vim.split(value, "") 879 | end 880 | end 881 | end 882 | end 883 | end --}}} 884 | 885 | -- version 2.1 886 | 887 | local function get_raw_parent_nodes(node) --{{{ 888 | local parents = {} 889 | 890 | while node:parent() do 891 | node = node:parent() 892 | 893 | table.insert(parents, node) 894 | end 895 | 896 | return parents 897 | end --}}} 898 | 899 | local function print_nodes_at_cursor() --{{{ 900 | local current_node = get_node_at_cursor() 901 | 902 | local parents = get_raw_parent_nodes(current_node) 903 | 904 | local types = { current_node:type() } 905 | for _, node in ipairs(parents) do 906 | table.insert(types, node:type()) 907 | end 908 | 909 | print(vim.inspect(types)) 910 | end --}}} 911 | 912 | vim.api.nvim_create_user_command("STSPrintNodesAtCursor", function() 913 | print_nodes_at_cursor() 914 | end, {}) 915 | 916 | -- version 2.2 917 | 918 | local held_node = nil --store the held node internally 919 | local function hold_node(node) --{{{ 920 | local bufnr = vim.api.nvim_get_current_buf() 921 | 922 | if node ~= nil then 923 | local end_row, end_col = node:end_() 924 | 925 | --clear old extmark 926 | if held_node and held_node.extmark_id then 927 | -- api.nvim_buf_del_extmark(0, ns, held_node.extmark_id) 928 | end 929 | 930 | -- store the held node with extra data for checks/extmark deletion 931 | held_node = { 932 | node = node, 933 | bufnr = bufnr, 934 | extmark_id = set_extmark_then_delete_it( -- set the extmark and save it for deletion 935 | end_row, 936 | end_col, 937 | " held node", 938 | M.opts.highlight_group, 939 | 8000 940 | ), 941 | } 942 | end 943 | end --}}} 944 | 945 | local function swap_held_node(node) --{{{ 946 | local bufnr = vim.api.nvim_get_current_buf() 947 | 948 | if held_node ~= nil and held_node.bufnr == bufnr then -- make sure we're swapping nodes in the same buffer 949 | ts_utils.swap_nodes(held_node.node, node, bufnr, true) 950 | api.nvim_buf_del_extmark(0, ns, held_node.extmark_id) --clear the extmark, probably don't need it after this 951 | held_node = nil 952 | else 953 | if held_node == nil then -- print out the reason for error 954 | print("No held node!") 955 | else 956 | print("Incorrect buffer!") 957 | end 958 | end 959 | end --}}} 960 | 961 | M.hold_or_swap = function(visual_mode) --{{{ 962 | if held_node == nil then 963 | if visual_mode then 964 | hold_node(get_visual_node()) 965 | else 966 | hold_node(get_node_at_cursor()) 967 | end 968 | else 969 | if visual_mode then 970 | swap_held_node(get_visual_node()) 971 | else 972 | swap_held_node(get_node_at_cursor()) 973 | end 974 | end 975 | end --}}} 976 | 977 | M.clear_held_node = function() 978 | if held_node and held_node.extmark_id then 979 | api.nvim_buf_del_extmark(0, ns, held_node.extmark_id) 980 | end 981 | held_node = nil 982 | end 983 | 984 | vim.api.nvim_create_user_command("STSSwapOrHold", function() 985 | M.hold_or_swap(false) 986 | end, {}) 987 | 988 | vim.api.nvim_create_user_command("STSSwapOrHoldVisual", function() 989 | M.hold_or_swap(true) 990 | vim.cmd("norm! ") 991 | end, {}) 992 | 993 | return M 994 | 995 | -- vim: foldmethod=marker foldmarker={{{,}}} foldlevel=0 996 | --------------------------------------------------------------------------------