Smart character movement (f,F,t,T) with hop.nvim
Published: Dec 19, 2024
Goal ΒΆ
Enhance Neovim's character navigation motions (f
, F
, t
, T
) with smart,
context-aware behavior. This enhancement integrates
hop.nvim to provide visual labels when
multiple target characters exist in the desired direction. The implementation
preserves Neovim's default behavior for single-character matches and
count-prefixed commands (like 3f
), while adding efficient multi-character
navigation through hop.nvim's labeling system.
-- Smart hop on `f`, `F`, `t`, and `T`
--
-- If there's only one of the target char in the direction specified, just go
-- there (default behavior). Otherwise, use hop to label the duplicates with
-- target labels
local function smart_hop(opts)
-- If there's a count, use default vim behavior
if vim.v.count > 0 then
local char = vim.fn.getchar()
char = type(char) == "number" and vim.fn.nr2char(char) or char
vim.cmd("normal! " .. vim.v.count .. opts.motion .. char)
return
end
-- Store if we're in operator-pending mode
local is_operator = vim.fn.mode(1):match("[vo]")
local hop = require("hop")
local hint = require("hop.hint")
local default_opts = setmetatable({}, { __index = require("hop.defaults") })
local jump_regex = require("hop.jump_regex")
local function check_opts(o)
if not o then
return
end
if vim.version.cmp({ 0, 10, 0 }, vim.version()) < 0 then
o.hint_type = hint.HintType.OVERLAY
end
end
local function override_opts(o)
check_opts(o)
return setmetatable(o or {}, { __index = default_opts })
end
local char = vim.fn.getchar()
char = type(char) == "number" and vim.fn.nr2char(char) or char
-- Get current line and cursor position
local line = vim.api.nvim_get_current_line()
local col = vim.api.nvim_win_get_cursor(0)[2]
-- Count occurrences based on direction
local count
if opts.direction == hint.HintDirection.AFTER_CURSOR then
local after_cursor = line:sub(col + 2)
count = select(2, after_cursor:gsub(vim.pesc(char), ""))
else
local before_cursor = line:sub(1, col + 1)
count = select(2, before_cursor:gsub(vim.pesc(char), ""))
end
if count <= 1 then
-- Use native motion for 0 or 1 occurrence
if is_operator then
-- In operator-pending mode, execute the operator with the motion
local op = vim.v.operator
local reg = vim.v.register ~= '"' and '"' .. vim.v.register or ""
vim.cmd("normal! " .. reg .. op .. opts.motion .. char)
else
-- In normal mode, just use the motion directly
vim.cmd("normal! " .. opts.motion .. char)
end
else
opts = override_opts({
direction = opts.direction,
current_line_only = true,
hint_offset = opts.hint_offset,
})
if is_operator then
-- For operators, we need to set the register and operator type
local reg = vim.v.register
local op = vim.v.operator
hop.hint_with_regex_opts = {
callback = function(pos)
-- Apply the operator from current position to target
local target_line = pos.line + 1
local target_col = pos.column + 1
local cmd = string.format(
"normal! %s%s%dl%dh",
reg ~= "" and '"' .. reg or "",
op,
target_line - vim.fn.line("."),
target_col - vim.fn.col(".")
)
vim.cmd(cmd)
end,
}
end
-- Use hop for multiple occurrences
hop.hint_with_regex(
jump_regex.regex_by_case_searching(char, true, opts), opts)
end
end
return {
"smoka7/hop.nvim",
version = "*",
config = true,
keys = {
{
"f",
function()
smart_hop({
direction = require("hop.hint").HintDirection.AFTER_CURSOR,
motion = "f",
hint_offset = 0,
})
end,
desc = "Smart hop char after cursor",
mode = { "n", "v", "o" },
},
{
"F",
function()
smart_hop({
direction = require("hop.hint").HintDirection.BEFORE_CURSOR,
motion = "F",
hint_offset = 0,
})
end,
desc = "Smart hop char before cursor",
mode = { "n", "v", "o" },
},
{
"t",
function()
smart_hop({
direction = require("hop.hint").HintDirection.AFTER_CURSOR,
motion = "t",
hint_offset = -1,
})
end,
desc = "Smart hop before char after cursor",
mode = { "n", "v", "o" },
},
{
"T",
function()
smart_hop({
direction = require("hop.hint").HintDirection.BEFORE_CURSOR,
motion = "T",
hint_offset = 1,
})
end,
desc = "Smart hop before char before cursor",
mode = { "n", "v", "o" },
},
},
}
Filed Under: