Skip to content

Commit

Permalink
- This is the 1st commit message:
Browse files Browse the repository at this point in the history
Smooth going until one major problem:

In order to swap nodes, we need to know how big they are.
We have the jump targets down pat, but knowing how big the current
node is is not easy. Because it's not obvious what the current node
is, only where its start position is.

- This is the commit message #2:

Iteration. Not working, but code structure is there

- This is the commit message #3:

Continuing iteration

Currently it's looking at lines. It's not working fully,
          something wrong in the swap logic. But it's looking at lines.
          It might be nice to do this by nodes, so instead of
          highest_coincident starting on the same line, looking at
          the highest coincident on the same line and col. And
          swapping like that as well.

- This is the commit message #4:

Pilfer ts_util's swap_nodes
  • Loading branch information
aaronik committed Dec 21, 2024
1 parent 7b1dfd1 commit f1cd25a
Show file tree
Hide file tree
Showing 15 changed files with 572 additions and 164 deletions.
88 changes: 8 additions & 80 deletions lua/treewalker/init.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
local nodes = require('treewalker.nodes')
local util = require('treewalker.util')
local ops = require('treewalker.ops')
local lines = require('treewalker.lines')
local strategies = require('treewalker.strategies')
local movement = require('treewalker.movement')
local swap = require('treewalker.swap')

local Treewalker = {}

Expand All @@ -22,84 +20,14 @@ function Treewalker.setup(opts)
end
end

---@return nil
function Treewalker.move_out()
local node = nodes.get_current()
local target = strategies.get_first_ancestor_with_diff_scol(node)
if not target then return end
local row = target:range()
row = row + 1
ops.jump(row, target)
-- Assign move_{in,out,up,down}
for fn_name, fn in pairs(movement) do
Treewalker[fn_name] = fn
end

---@return nil
function Treewalker.move_in()
local current_row = vim.fn.line(".")
local current_line = lines.get_line(current_row)
local current_col = lines.get_start_col(current_line)

--- Go down and in
local candidate, candidate_row, candidate_line =
strategies.get_down_and_in(current_row, current_col)

-- Ultimate failure
if not candidate_row or not candidate_line or not candidate then
return --util.log("no in candidate")
end

ops.jump(candidate_row, candidate)
end

---@return nil
function Treewalker.move_up()
local current_row = vim.fn.line(".")
local current_line = lines.get_line(current_row)
local current_col = lines.get_start_col(current_line)

-- Get next target if we're on an empty line
local candidate, candidate_row, candidate_line =
strategies.get_prev_if_on_empty_line(current_row, current_line)

if candidate_row and candidate_line and candidate then
return ops.jump(candidate_row, candidate)
end

--- Get next target at the same column
candidate, candidate_row, candidate_line =
strategies.get_neighbor_at_same_col("up", current_row, current_col)

if candidate_row and candidate_line and candidate then
return ops.jump(candidate_row, candidate)
end

-- Ultimate failure
-- return util.log("no up candidate")
end

---@return nil
function Treewalker.move_down()
local current_row = vim.fn.line(".")
local current_line = lines.get_line(current_row)
local current_col = lines.get_start_col(current_line)

-- Get next target if we're on an empty line
local candidate, candidate_row, candidate_line =
strategies.get_next_if_on_empty_line(current_row, current_line)

if candidate_row and candidate_line and candidate then
return ops.jump(candidate_row, candidate)
end

--- Get next target, if one is found
candidate, candidate_row, candidate_line =
strategies.get_neighbor_at_same_col("down", current_row, current_col)

if candidate_row and candidate_line and candidate then
return ops.jump(candidate_row, candidate)
end

-- Ultimate failure
-- return util.log("no down candidate")
-- Assign swap_{up,down}
for fn_name, fn in pairs(swap) do
Treewalker[fn_name] = fn
end

return Treewalker
38 changes: 38 additions & 0 deletions lua/treewalker/lines.lua
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
local M = {}

---@param row integer
---@param line string
function M.set_line(row, line)
vim.api.nvim_buf_set_lines(0, row - 1, row, false, { line })
end

-- Insert an arbitrary number of lines into the doc, without overwriting any
---@param start integer
---@param lines string[]
function M.insert_lines(start, lines)
for i, line in ipairs(lines) do
vim.api.nvim_buf_set_lines(0, start + i - 1, start + i - 1, false, { line })
end
end

---@param start integer
---@param lines string[]
function M.set_lines(start, lines)
local fin = start + #lines - 1
vim.api.nvim_buf_set_lines(0, start - 1, fin, false, lines)
end

---@param row integer
function M.get_line(row)
return vim.api.nvim_buf_get_lines(0, row - 1, row, false)[1]
end

---@param start integer
---@param fin integer
function M.get_lines(start, fin)
local lines = {}
for row = start, fin, 1 do
table.insert(lines, M.get_line(row))
end
return lines
end

---@param start integer
---@param fin integer
function M.delete_lines(start, fin)
return vim.api.nvim_buf_set_lines(0, start - 1, fin, false, {})
end

---@param line string
---@return integer
function M.get_start_col(line)
Expand Down
46 changes: 46 additions & 0 deletions lua/treewalker/movement.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
local ops = require "treewalker.ops"
local targets = require "treewalker.targets"

local M = {}

---@return nil
function M.move_out()
local target, row, line = targets.out()
if target and row and line then
--util.log("no out candidate")
ops.jump(row, target)
return
end
end

---@return nil
function M.move_in()
local target, row, line = targets.inn()

if target and row and line then
--util.log("no in candidate")
ops.jump(row, target)
end
end

---@return nil
function M.move_up()
local target, row, line = targets.up()

if target and row and line then
--util.log("no up candidate")
ops.jump(row, target)
end
end

---@return nil
function M.move_down()
local target, row, line = targets.down()

if target and row and line then
--util.log("no down candidate")
ops.jump(row, target)
end
end

return M
6 changes: 6 additions & 0 deletions lua/treewalker/nodes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,10 @@ function M.get_at_row(row)
return vim.treesitter.get_node({ pos = { row - 1, col } })
end

-----Getting the size of the node is no trivial thing. Different nodes behave differently.
-----@param row integer
-----@return TSNode|nil
--function M.get_real_range(row)
--end

return M
107 changes: 106 additions & 1 deletion lua/treewalker/ops.lua
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ end
function M.jump(row, 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
vim.cmd("normal! ^") -- Jump to start of line
if require("treewalker").opts.highlight then
node = nodes.get_highest_coincident(node)
local range = nodes.range(node)
Expand All @@ -63,4 +63,109 @@ function M.jump(row, node)
end
end

---@param n integer
---@return ""[]
local function n_empty_lines(n)
local result = {}
for _ = 1, n do
table.insert(result, "")
end
return result
end

---@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 earlier_start, earlier_end, earlier_text
local later_start, later_end, later_text

local _ = {
hi = "bye",
}

_ = {
bye = "hi",
good = "night"
}

-- Sort out which of the given nodes is the earlier vs later one
-- TODO Can this be done by always passing in earlier/later in the right order?
-- Seems like move_up / move_down should know which is earlier / later
if s1 < s2 then
earlier_start = s1
earlier_end = e1
earlier_text = lines.get_lines(s1 + 1, e1 + 1)
later_start = s2
later_end = e2
later_text = lines.get_lines(s2 + 1, e2 + 1)
else
later_start = s1
later_end = e1
later_text = lines.get_lines(s1 + 1, e1 + 1)
earlier_start = s2
earlier_end = e2
earlier_text = lines.get_lines(s2 + 1, e2 + 1)
end

util.log("earlier: " .. earlier_start .. "/" .. earlier_end)
util.log("later: " .. later_start .. "/" .. later_end)

-- Collapse the later node
lines.delete_lines(later_start + 1, later_end + 1) -- two plus ones works for deleting single and multiple lines

-- Add earlier node to later slot
lines.insert_lines(later_start, earlier_text)

-- Now collapse the earlier node
lines.delete_lines(earlier_start + 1, earlier_end + 1)

-- And add the later node to the earlier slot
lines.insert_lines(earlier_start, later_text)
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


57 changes: 57 additions & 0 deletions lua/treewalker/swap.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
local nodes = require "treewalker.nodes"
local ops = require "treewalker.ops"
local targets = require "treewalker.targets"

local M = {}

---@param target TSNode
local function swap_with_current(target)
local current = nodes.get_current()
current = nodes.get_highest_coincident(current)
target = nodes.get_highest_coincident(target)

local target_range = nodes.range(target)
local current_range = nodes.range(current)

ops.swap(
{ current_range[1], current_range[3] },
{ target_range[1], target_range[3] }
)

-- cursor should follow current node
-- TODO This should be factored out into the swap_down and swap_up
if current_range[1] < target_range[1] then
-- for down swaps
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)
elseif current_range[1] > target_range[1] then
-- up swaps
vim.fn.cursor(target_range[1] + 1, target_range[2] + 1)
end
end

---@return nil
function M.swap_down()
local target, row, line = targets.down()

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

swap_with_current(target)
end

---@return nil
function M.swap_up()
local target, row, line = targets.up()

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

swap_with_current(target)
end

return M
Loading

0 comments on commit f1cd25a

Please sign in to comment.