Skip to content

Commit

Permalink
I thiiiink everything works except for cursor placement
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronik committed Dec 23, 2024
1 parent 1f1ce7a commit 1c53e77
Show file tree
Hide file tree
Showing 15 changed files with 330 additions and 132 deletions.
44 changes: 36 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,27 @@

Treewalker is a plugin that gives you the ability to **move around your code in a syntax tree aware manner**.
It uses [Treesitter](https://github.com/tree-sitter/tree-sitter) under the hood for syntax tree awareness.
It offers four subcommands: Up, Down, Right, and Left. Each command moves through the syntax tree
in an intuitive way.
It offers six subcommands: Up, Down, Right, and Left for movement, and SwapUp and SwapDown for intelligent node swapping.

Each movement command moves you through the syntax tree in an intuitive way.

* **Up/Down** - Moves up or down to the next neighbor node
* **Right** - Finds the next good child node
* **Left** - Finds the next good parent node

The swap commands intelligently swap nodes, including comments and attributes/decorators.

---

Moving slowly, showing each command
![A demo of moving around some code slowly typing out each command](static/slow_demo.gif)
<details>
<summary>Typing out the Move commands manually</summary>
<img src="static/slow_move_demo.gif" alt="A demo of moving around some code slowly typing out each Treewalker move command">
</details>

<details>
<summary>Typing out the Swap commands manually</summary>
<img src="static/slow_swap_demo.gif" alt="A demo of swapping code slowly using Treewalker swap commands">
</details>

---

Expand All @@ -42,8 +52,26 @@ Moving slowly, showing each command
This is how I have mine mapped; in `init.lua`:

```lua
vim.api.nvim_set_keymap('n', '<C-j>', ':Treewalker Down<CR>', { noremap = true })
vim.api.nvim_set_keymap('n', '<C-k>', ':Treewalker Up<CR>', { noremap = true })
vim.api.nvim_set_keymap('n', '<C-h>', ':Treewalker Left<CR>', { noremap = true })
vim.api.nvim_set_keymap('n', '<C-l>', ':Treewalker Right<CR>', { noremap = true })
vim.keymap.set({ 'n', 'v' }, '<C-k>', '<cmd>Treewalker Up<cr>', { noremap = true, silent = true })
vim.keymap.set({ 'n', 'v' }, '<C-j>', '<cmd>Treewalker Down<cr>', { noremap = true, silent = true })
vim.keymap.set({ 'n', 'v' }, '<C-l>', '<cmd>Treewalker Right<cr>', { noremap = true, silent = true })
vim.keymap.set({ 'n', 'v' }, '<C-h>', '<cmd>Treewalker Left<cr>', { noremap = true, silent = true })
vim.keymap.set('n', '<C-S-j>', '<cmd>Treewalker SwapDown<cr>', { noremap = true, silent = true })
vim.keymap.set('n', '<C-S-k>', '<cmd>Treewalker SwapUp<cr>', { noremap = true, silent = true })
```

I also utilize some
[nvim-treesitter-textobjects](https://github.com/nvim-treesitter/nvim-treesitter-textobjects?tab=readme-ov-file#text-objects-swap)
commands to round out the swap commands - `<C-S-{j,k,l,h}>` just feel really good to me, so might as well get the
lateral swapping as well. (This is not something that `Treewalker` needs to do as it already exists from other libraries)

```lua
vim.keymap.set('n', "<C-S-l>", ":TSTextobjectSwapNext @parameter.inner<CR>", { noremap = true, silent = true })
vim.keymap.set('n', "<C-S-h>", ":TSTextobjectSwapPrevious @parameter.inner<CR>", { noremap = true, silent = true })
```

The above can also be accomplished with
[nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) using
[ts_utils](https://github.com/nvim-treesitter/nvim-treesitter?tab=readme-ov-file#utilities).
See [this PR](https://github.com/aaronik/treewalker.nvim/pull/10/files) for
an example of that!
3 changes: 0 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
# TODO

* Check for errant util.log or util.R usages in CI/local
* Swapping
* Jumplist
* Get more languages into movement/highlight/swap specs
* :help treesitter-parsers
* Python decorators (tough b/c of the identifier node underneath)
2 changes: 2 additions & 0 deletions lua/treewalker/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ function Treewalker.setup(opts)
end
end

-- TODO This is clever kinda, but it breaks autocomplete of `require('treewalker')`

-- Assign move_{in,out,up,down}
for fn_name, fn in pairs(movement) do
Treewalker[fn_name] = fn
Expand Down
12 changes: 6 additions & 6 deletions lua/treewalker/nodes.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
local lines = require "treewalker.lines"
local util = require "treewalker.util"
local lines = require "treewalker.lines"
local util = require "treewalker.util"

-- These are regexes but just happen to be real simple so far
local TARGET_BLACKLIST_TYPE_MATCHERS = {
local TARGET_BLACKLIST_TYPE_MATCHERS = {
"comment",
"attribute_item", -- decorators (rust)
"decorat", -- decorators (py)
Expand All @@ -15,13 +15,13 @@ local HIGHLIGHT_BLACKLIST_TYPE_MATCHERS = {
"block",
}

local AUGMENT_TARGET_TYPE_MATCHERS = {
local AUGMENT_TARGET_TYPE_MATCHERS = {
"comment",
"attribute_item", -- decorators (rust)
"decorat", -- decorators (py)
}

local M = {}
local M = {}

---@param node TSNode
---@param matchers string[]
Expand Down Expand Up @@ -232,7 +232,7 @@ function M.log(node)
local row = M.range(node)[1] + 1
local line = lines.get_line(row)
local col = lines.get_start_col(line)
local log_string = "dest:"
local log_string = ""
log_string = log_string .. string.format(" [%s/%s]", row, col)
log_string = log_string .. string.format(" (%s)", node:type())
log_string = log_string .. string.format(" |%s|", line)
Expand Down
60 changes: 4 additions & 56 deletions lua/treewalker/ops.lua
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
local util = require('treewalker.util')
local nodes = require('treewalker.nodes')
local lines = require('treewalker.lines')
local augment = require('treewalker.augment')

local M = {}

-- For a potentially more nvim-y way to do it, see how treesitter-utils does it:
-- https://github.com/nvim-treesitter/nvim-treesitter/blob/981ca7e353da6ea69eaafe4348fda5e800f9e1d8/lua/nvim-treesitter/ts_utils.lua#L388
-- (ts_utils.swap_nodes)

---Flash a highlight over the given range
---@param range Range4
---@param duration integer
function M.highlight(range, duration)
local start_row, start_col, end_row, end_col = range[1], range[2], range[3], range[4]
local ns_id = vim.api.nvim_create_namespace("")
-- local hl_group = "DiffAdd"
-- local hl_group = "MatchParen"
-- local hl_group = "Search"
local hl_group = "ColorColumn"

for row = start_row, end_row do
Expand Down Expand Up @@ -41,22 +40,13 @@ end
---@param row integer
---@param node TSNode
function M.jump(row, node)
nodes.log(node)
vim.cmd("normal! m'") -- Add originating node to jump list
vim.api.nvim_win_set_cursor(0, { row, 0 })
vim.cmd("normal! ^") -- Jump to start of line
if require("treewalker").opts.highlight then
local range = nodes.range(node)
local duration = require("treewalker").opts.highlight_duration
M.highlight(range, duration)

-- -- TODO Make this not look like butt, if it's even wanted
-- local augments = augment.get_node_augments(node)
-- for _, aug in ipairs(augments) do
-- range = nodes.range(aug)
-- M.highlight(range, duration)
-- end

end
end

Expand All @@ -83,45 +73,3 @@ end

return M

---- Leaving this here for now because my gut says this is a better way to do it,
---- and at some point it may want to get done.
---- https://github.com/nvim-treesitter/nvim-treesitter/blob/981ca7e353da6ea69eaafe4348fda5e800f9e1d8/lua/nvim-treesitter/ts_utils.lua#L388
---- (ts_utils.swap_nodes)
-----@param rows1 [integer, integer] -- [start row, end row]
-----@param rows2 [integer, integer] -- [start row, end row]
--function M.swap(rows1, rows2)
-- local s1, e1, s2, e2 = rows1[1], rows1[2], rows2[1], rows2[2]
-- local text1 = lines.get_lines(s1 + 1, e1 + 1)
-- local text2 = lines.get_lines(s2 + 1, e2 + 1)

-- util.log("text1: " .. s1 .. "/" .. e1)
-- util.log("text2: " .. s2 .. "/" .. e2)

-- ---@type lsp.Range
-- local range1 = {
-- start = { line = s1, character = 0 },
-- ["end"] = { line = e1, character = 0 } -- end is reserved
-- }

-- ---@type lsp.Range
-- local range2 = {
-- start = { line = s2, character = 0 },
-- ["end"] = { line = e2 + 1, character = 0 }
-- }

-- -- util.log(range1, range2)

-- -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEdit
-- lines.set_lines(s1 + 1, text2)
-- ---@type lsp.TextEdit
-- local edit1 = { range = range1, newText = table.concat(text2, "\n") }

-- lines.set_lines(s2 + 1, text1)
-- ---@type lsp.TextEdit
-- local edit2 = { range = range2, newText = table.concat(text1, "\n") }

-- local bufnr = vim.api.nvim_get_current_buf()
-- -- vim.lsp.util.apply_text_edits({ edit1, edit2 }, bufnr, "utf-8")
--end


67 changes: 52 additions & 15 deletions lua/treewalker/swap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,66 @@ local nodes = require "treewalker.nodes"
local ops = require "treewalker.ops"
local targets = require "treewalker.targets"
local augment = require "treewalker.augment"
local util = require "treewalker.util"

local M = {}

---@return nil
---@return boolean
local function is_on_target_node()
local node = vim.treesitter.get_node()
if not node then return false end
if not nodes.is_jump_target(node) then return false end
if vim.fn.line('.') - 1 ~= node:range() then return false end
return true
end

---@return boolean
local function is_supported_ft()
local unsupported_filetypes = {
["text"] = true,
["markdown"] = true,
}

local bufnr = vim.api.nvim_get_current_buf()
local ft = vim.bo[bufnr].filetype

return not unsupported_filetypes[ft]
end

function M.swap_down()
if not is_on_target_node() then return end
if not is_supported_ft() then return end

local target, row, line = targets.down()

if not target or not row or not line then
--util.log("no down candidate")
return
end

-- TODO remove this comment it's just for testing fr fr
local current = nodes.get_current()
local current_range = nodes.range(current)
local all = augment.get_node_augments(current)
table.insert(all, current)
local current_rows = nodes.row_range(all)
local current_augments = augment.get_node_augments(current)
local current_all = { current, unpack(current_augments) }
local current_all_rows = nodes.row_range(current_all)

local target_range = nodes.range(target)
local target_rows = { target_range[1], target_range[3] }
local target_augments = augment.get_node_augments(target)
local target_all = { target, unpack(target_augments) }
local target_all_rows = nodes.row_range(target_all)

ops.swap(current_rows, target_rows)
ops.swap(current_all_rows, target_all_rows)

-- Place cursor
local node_length_diff = ((current_range[3] - current_range[1]) + 1) - ((target_range[3] - target_range[1]) + 1)
vim.fn.cursor(target_range[1] - node_length_diff + 1, target_range[2] + 1)
local x = target_range[1] - node_length_diff + 1
local y = target_range[2] + 1
vim.fn.cursor(x, y)
end

---@return nil
function M.swap_up()
if not is_on_target_node() then return end
if not is_supported_ft() then return end

local target, row, line = targets.up()

if not target or not row or not line then
Expand All @@ -42,16 +70,25 @@ function M.swap_up()
end

local current = nodes.get_current()
local current_range = nodes.range(current)
local current_rows = { current_range[1], current_range[3] }
local current_augments = augment.get_node_augments(current)
local current_all = { current, unpack(current_augments) }
local current_all_rows = nodes.row_range(current_all)

local target_range = nodes.range(target)
local target_rows = { target_range[1], target_range[3] }
local target_augments = augment.get_node_augments(target)
local target_all = { target, unpack(target_augments) }
local target_all_rows = nodes.row_range(target_all)

ops.swap(target_rows, current_rows)
ops.swap(target_all_rows, current_all_rows)

-- Place cursor
vim.fn.cursor(target_range[1] + 1, target_range[2] + 1)
local target_augment_rows = nodes.row_range(target_augments)
local target_augment_length = #target_augments > 0 and (target_augment_rows[2] + 1 - target_augment_rows[1]) or 0
local current_augment_rows = nodes.row_range(current_augments)
local current_augment_length = #current_augments > 0 and (current_augment_rows[2] + 1 - current_augment_rows[1]) or 0
local x = target_range[1] + 1 + current_augment_length - target_augment_length
local y = target_range[2] + 1
vim.fn.cursor(x, y)
end

return M
8 changes: 0 additions & 8 deletions plugin/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,6 @@ local subcommands = {

SwapDown = function()
tw().swap_down()
end,

SwapRight = function()
tw().swap_in()
end,

SwapLeft = function()
tw().swap_out()
end
}

Expand Down
Binary file modified static/fast_demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Binary file added static/slow_swap_demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 13 additions & 11 deletions tests/fixtures/javascript.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,53 @@
// What is this hot garbo
function myCallback1(err, data) {
if (err) return console.error(err);
console.log('myCallback1:', data);
}

function myCallback2(data) {
console.log('myCallback2:', data * 2);
console.log('myCallback2:', data * 2); // Magic number
}

// Real descriptive name
// I definitely know what this function is doing... /s
function myCallback3(data) {
console.log('myCallback3:', data - 10);
console.log('myCallback3:', data - 10); // Another magic number
}

// Some people comment their ai generated slop

function fetchData(callback) {
setTimeout(() => {
const data = { foo: 'bar' };
callback(null, data);
}, 1000);
}, 1000); // Ever heard of constants
}

// Who wrote this terrible code
function processData(data, callback) {
myCallback1(null, data); // callback within a callback!
myCallback1(null, data); // who can even follow this many callbacks
const processedData = { baz: data.foo + 'qux' };
setTimeout(() => {
myCallback2(processedData); // another callback
myCallback2(processedData); // good thing this isn't typed
callback(null, processedData);
}, 500);
}

// Process what data jeez I think this code isn't even real
function processMoreData(data, callback) {
const moreProcessedData = { quux: data.baz - 1 };
myCallback3(moreProcessedData); // yet another callback
myCallback3(moreProcessedData);
setTimeout(() => {
callback(null, moreProcessedData);
}, 250);
}

// Some people prefer to live dangerously

// If I saw this in prod I'd have a conniption
fetchData((err, data) => {
if (err) return console.error(err);
processData(data, (err, processedData) => {
if (err) return console.error(err);
processMoreData(processedData, (err, moreProcessedData) => {
if (err) return console.error(err);
myCallback1(null, moreProcessedData); // final countdown
myCallback1(null, moreProcessedData);
});
});
});
Expand Down
Loading

0 comments on commit 1c53e77

Please sign in to comment.