Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repeat movements like ; and , #359

Merged
merged 8 commits into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions lua/nvim-treesitter/textobjects/attach.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ local parsers = require "nvim-treesitter.parsers"
local queries = require "nvim-treesitter.query"
local M = {}

local function make_repeatable(fn)
local function make_dot_repeatable(fn)
return function()
_G._nvim_treesitter_textobject_last_function = fn
vim.o.opfunc = "v:lua._nvim_treesitter_textobject_last_function"
vim.api.nvim_feedkeys("g@l", "n", false)
end
end

function M.make_attach(normal_mode_functions, submodule, keymap_modes, opts)
function M.make_attach(functions, submodule, keymap_modes, opts)
keymap_modes = keymap_modes or "n"
opts = opts or {}
return function(bufnr, lang)
Expand All @@ -22,7 +22,7 @@ function M.make_attach(normal_mode_functions, submodule, keymap_modes, opts)

local config = configs.get_module("textobjects." .. submodule)

for _, function_call in pairs(normal_mode_functions) do
for _, function_call in pairs(functions) do
local function_description = function_call:gsub("_", " "):gsub("^%l", string.upper)
for mapping, query_metadata in pairs(config[function_call] or {}) do
local mapping_description, query
Expand All @@ -38,8 +38,8 @@ function M.make_attach(normal_mode_functions, submodule, keymap_modes, opts)
local fn = function()
require("nvim-treesitter.textobjects." .. submodule)[function_call](query)
end
if opts.repeatable then
fn = make_repeatable(fn)
if opts.dot_repeatable then
fn = make_dot_repeatable(fn)
end
vim.keymap.set(
keymap_modes,
Expand All @@ -52,7 +52,7 @@ function M.make_attach(normal_mode_functions, submodule, keymap_modes, opts)
end
end

function M.make_detach(normal_mode_functions, submodule, keymap_modes)
function M.make_detach(functions, submodule, keymap_modes)
keymap_modes = keymap_modes or "n"
return function(bufnr)
local config = configs.get_module("textobjects." .. submodule)
Expand All @@ -66,7 +66,7 @@ function M.make_detach(normal_mode_functions, submodule, keymap_modes)
vim.keymap.del({ "o", "x" }, mapping, { buffer = bufnr })
end
end
for _, function_call in pairs(normal_mode_functions) do
for _, function_call in pairs(functions) do
for mapping, query in pairs(config[function_call] or {}) do
if type(query) == "table" then
query = query[lang]
Expand Down
205 changes: 200 additions & 5 deletions lua/nvim-treesitter/textobjects/move.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,53 @@ local configs = require "nvim-treesitter.configs"

local M = {}

local function move(query_strings, forward, start, winid)
M.last_move = nil
-- { func = move, args = { ... } }
-- { func = builtin_find, args = { ... } }
-- register any other function, but make sure the the first args is `forward` boolean.

-- If you are a plugin developer,
-- you can register your own independent move functions using this
-- even if they are not related to treesitter-textobjects.
-- move_fn's first argument must be a boolean indicating whether to move forward (true) or backward (false)
-- Then you can use four functions:
-- M.repeat_last_move
-- M.repeat_last_move_opposite
-- M.repeat_last_move_next
-- M.repeat_last_move_previous
function M.make_repeatable_move(move_fn)
return function(...)
M.last_move = { func = move_fn, args = { ... } } -- remember that the first args should be `forward` boolean
move_fn(...)
end
end

-- Alternatively, you can use this function
-- Returns:
-- repeatable_forward_move_fn, repeatable_backward_move_fn
function M.make_repeatable_move_pair(forward_move_fn, backward_move_fn)
local general_repeatable_move_fn = function(forward, ...)
if forward then
forward_move_fn(...)
else
backward_move_fn(...)
end
end

local repeatable_forward_move_fn = function(...)
M.last_move = { func = general_repeatable_move_fn, args = { true, ... } }
forward_move_fn(...)
end

local repeatable_backward_move_fn = function(...)
M.last_move = { func = general_repeatable_move_fn, args = { false, ... } }
backward_move_fn(...)
end

return repeatable_forward_move_fn, repeatable_backward_move_fn
end

local function move(forward, query_strings, start, winid)
query_strings = shared.make_query_strings_table(query_strings)
winid = winid or vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf(winid)
Expand Down Expand Up @@ -69,17 +115,142 @@ local function move(query_strings, forward, start, winid)
end
end

local move_repeatable = M.make_repeatable_move(move)

M.goto_next_start = function(query_strings)
theHamsta marked this conversation as resolved.
Show resolved Hide resolved
move(query_strings, "forward", "start")
move_repeatable("forward", query_strings, "start")
end
M.goto_next_end = function(query_strings)
move(query_strings, "forward", not "start")
move_repeatable("forward", query_strings, not "start")
end
M.goto_previous_start = function(query_strings)
move(query_strings, not "forward", "start")
move_repeatable(not "forward", query_strings, "start")
end
M.goto_previous_end = function(query_strings)
move(query_strings, not "forward", not "start")
move_repeatable(not "forward", query_strings, not "start")
end

-- implements naive f, F, t, T with repeat support
local function builtin_find(forward, inclusive, char, repeating, winid)
-- forward = true -> f, t
-- inclusive = false -> t, T
-- if repeating with till (t or T, inclusive = false) then search from the next character
-- returns nil if cancelled or char
char = char or vim.fn.nr2char(vim.fn.getchar())
repeating = repeating or false
winid = winid or vim.api.nvim_get_current_win()

if char == vim.fn.nr2char(27) then
-- escape
return nil
end

local line = vim.api.nvim_get_current_line()
local cursor = vim.api.nvim_win_get_cursor(winid)

-- find the count-th occurrence of the char in the line
local found
for _ = 1, vim.v.count1 do
theHamsta marked this conversation as resolved.
Show resolved Hide resolved
if forward then
if not inclusive and repeating then
cursor[2] = cursor[2] + 1
end
found = line:find(char, cursor[2] + 2, true)
else
-- reverse find from the cursor position
if not inclusive and repeating then
cursor[2] = cursor[2] - 1
end

found = line:reverse():find(char, #line - cursor[2] + 1, true)
if found then
found = #line - found + 1
end
end

if not found then
return char
end

if forward then
if not inclusive then
found = found - 1
end
else
if not inclusive then
found = found + 1
end
end

cursor[2] = found - 1
repeating = true -- after the first iteration, search from the next character if not inclusive.
end

-- move to the found position
vim.api.nvim_win_set_cursor(winid, { cursor[1], cursor[2] })
return char
end

-- We are not using M.make_repeatable_move and instead registering at M.last_move manually
-- because we don't want to behave the same way as the first movement.
-- For example, we want to repeat the search character given to f, F, t, T.
-- Also, we want to be able to to find the next occurence when using t, T with repeat, excluding the current position.
M.builtin_f = function()
local char = builtin_find("forward", "inclusive")
if builtin_find ~= nil then
M.last_move = { func = builtin_find, args = { "forward", "inclusive", char, "repeating" } }
end
end

M.builtin_F = function()
local char = builtin_find(not "forward", "inclusive")
if builtin_find ~= nil then
M.last_move = { func = builtin_find, args = { not "forward", "inclusive", char, "repeating" } }
end
end

M.builtin_t = function()
local char = builtin_find("forward", not "inclusive")
if builtin_find ~= nil then
M.last_move = { func = builtin_find, args = { "forward", not "inclusive", char, "repeating" } }
end
end

M.builtin_T = function()
local char = builtin_find(not "forward", not "inclusive")
if builtin_find ~= nil then
M.last_move = { func = builtin_find, args = { not "forward", not "inclusive", char, "repeating" } }
end
end

M.repeat_last_move = function()
if M.last_move then
M.last_move.func(unpack(M.last_move.args))
end
end

M.repeat_last_move_opposite = function()
if M.last_move then
local args = { unpack(M.last_move.args) } -- copy the table
args[1] = not args[1] -- reverse the direction
M.last_move.func(unpack(args))
end
end

M.repeat_last_move_next = function()
if M.last_move then
local args = { unpack(M.last_move.args) } -- copy the table
args[1] = true -- set the direction to forward
M.last_move.func(unpack(args))
end
end

M.repeat_last_move_previous = function()
if M.last_move then
local args = { unpack(M.last_move.args) } -- copy the table
args[1] = false -- set the direction to backward
M.last_move.func(unpack(args))
end
end

local nxo_mode_functions = { "goto_next_start", "goto_next_end", "goto_previous_start", "goto_previous_end" }
Expand Down Expand Up @@ -116,6 +287,30 @@ M.commands = {
"-complete=custom,nvim_treesitter_textobjects#available_textobjects",
},
},
theHamsta marked this conversation as resolved.
Show resolved Hide resolved
TSTextobjectRepeatLastMove = {
run = M.repeat_last_move,
},
TSTextobjectRepeatLastMoveOpposite = {
run = M.repeat_last_move_opposite,
},
TSTextobjectRepeatLastMoveNext = {
run = M.repeat_last_move_next,
},
TSTextobjectRepeatLastMovePrevious = {
run = M.repeat_last_move_previous,
},
TSTextobjectBuiltinf = {
run = M.builtin_f,
},
TSTextobjectBuiltinF = {
run = M.builtin_F,
},
TSTextobjectBuiltint = {
run = M.builtin_t,
},
TSTextobjectBuiltinT = {
run = M.builtin_T,
},
}

return M
2 changes: 1 addition & 1 deletion lua/nvim-treesitter/textobjects/swap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ end

local normal_mode_functions = { "swap_next", "swap_previous" }

M.attach = attach.make_attach(normal_mode_functions, "swap", "n", { repeatable = true })
M.attach = attach.make_attach(normal_mode_functions, "swap", "n", { dot_repeatable = true })
M.detach = attach.make_detach(normal_mode_functions, "swap", "n")

M.commands = {
Expand Down