Problem: inner_task_range used unescaped '-' in Lua patterns, which acts as a lazy quantifier instead of matching a literal hyphen. The metadata-stripping logic also tokenized the full line including the prefix, so the rebuilt string could never be found after the prefix. All test column expectations were off by one. Solution: escape hyphens with %-, rewrite metadata stripping to tokenize only the description portion after the prefix, and correct all test assertions to match actual rendered column positions.
350 lines
7.3 KiB
Lua
350 lines
7.3 KiB
Lua
local buffer = require('pending.buffer')
|
|
local config = require('pending.config')
|
|
|
|
---@class pending.textobj
|
|
local M = {}
|
|
|
|
---@param lnum integer
|
|
---@param meta pending.LineMeta[]
|
|
---@return string
|
|
local function get_line_from_buf(lnum, meta)
|
|
local _ = meta
|
|
local bufnr = buffer.bufnr()
|
|
if not bufnr then
|
|
return ''
|
|
end
|
|
local lines = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false)
|
|
return lines[1] or ''
|
|
end
|
|
|
|
---@param line string
|
|
---@return integer start_col
|
|
---@return integer end_col
|
|
function M.inner_task_range(line)
|
|
local prefix_end = line:find('/') and select(2, line:find('^/%d+/%- %[.%] '))
|
|
if not prefix_end then
|
|
prefix_end = select(2, line:find('^%- %[.%] ')) or 0
|
|
end
|
|
local start_col = prefix_end + 1
|
|
|
|
local dk = config.get().date_syntax or 'due'
|
|
local rk = config.get().recur_syntax or 'rec'
|
|
local dk_pat = '^' .. vim.pesc(dk) .. ':%S+$'
|
|
local rk_pat = '^' .. vim.pesc(rk) .. ':%S+$'
|
|
|
|
local rest = line:sub(start_col)
|
|
local words = {}
|
|
for word in rest:gmatch('%S+') do
|
|
table.insert(words, word)
|
|
end
|
|
|
|
local i = #words
|
|
while i >= 1 do
|
|
local word = words[i]
|
|
if word:match(dk_pat) or word:match('^cat:%S+$') or word:match(rk_pat) then
|
|
i = i - 1
|
|
else
|
|
break
|
|
end
|
|
end
|
|
|
|
if i < 1 then
|
|
return start_col, start_col
|
|
end
|
|
|
|
local desc = table.concat(words, ' ', 1, i)
|
|
local end_col = start_col + #desc - 1
|
|
return start_col, end_col
|
|
end
|
|
|
|
---@param row integer
|
|
---@param meta pending.LineMeta[]
|
|
---@return integer? header_row
|
|
---@return integer? last_row
|
|
function M.category_bounds(row, meta)
|
|
if not meta or #meta == 0 then
|
|
return nil, nil
|
|
end
|
|
|
|
local header_row = nil
|
|
local m = meta[row]
|
|
if not m then
|
|
return nil, nil
|
|
end
|
|
|
|
if m.type == 'header' then
|
|
header_row = row
|
|
else
|
|
for r = row, 1, -1 do
|
|
if meta[r] and meta[r].type == 'header' then
|
|
header_row = r
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if not header_row then
|
|
return nil, nil
|
|
end
|
|
|
|
local last_row = header_row
|
|
local total = #meta
|
|
for r = header_row + 1, total do
|
|
if meta[r].type == 'header' then
|
|
break
|
|
end
|
|
last_row = r
|
|
end
|
|
|
|
return header_row, last_row
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.a_task(count)
|
|
local meta = buffer.meta()
|
|
if not meta or #meta == 0 then
|
|
return
|
|
end
|
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
|
local m = meta[row]
|
|
if not m or m.type ~= 'task' then
|
|
return
|
|
end
|
|
|
|
local start_row = row
|
|
local end_row = row
|
|
count = math.max(1, count)
|
|
for _ = 2, count do
|
|
local next_row = end_row + 1
|
|
if next_row > #meta then
|
|
break
|
|
end
|
|
if meta[next_row] and meta[next_row].type == 'task' then
|
|
end_row = next_row
|
|
else
|
|
break
|
|
end
|
|
end
|
|
|
|
vim.cmd('normal! ' .. start_row .. 'GV' .. end_row .. 'G')
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.a_task_visual(count)
|
|
vim.cmd('normal! \27')
|
|
M.a_task(count)
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.i_task(count)
|
|
local _ = count
|
|
local meta = buffer.meta()
|
|
if not meta or #meta == 0 then
|
|
return
|
|
end
|
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
|
local m = meta[row]
|
|
if not m or m.type ~= 'task' then
|
|
return
|
|
end
|
|
|
|
local line = get_line_from_buf(row, meta)
|
|
local start_col, end_col = M.inner_task_range(line)
|
|
if start_col > end_col then
|
|
return
|
|
end
|
|
|
|
vim.api.nvim_win_set_cursor(0, { row, start_col - 1 })
|
|
vim.cmd('normal! v')
|
|
vim.api.nvim_win_set_cursor(0, { row, end_col - 1 })
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.i_task_visual(count)
|
|
vim.cmd('normal! \27')
|
|
M.i_task(count)
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.a_category(count)
|
|
local _ = count
|
|
local meta = buffer.meta()
|
|
if not meta or #meta == 0 then
|
|
return
|
|
end
|
|
local view = buffer.current_view_name()
|
|
if view == 'priority' then
|
|
return
|
|
end
|
|
|
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
|
local header_row, last_row = M.category_bounds(row, meta)
|
|
if not header_row or not last_row then
|
|
return
|
|
end
|
|
|
|
local start_row = header_row
|
|
if header_row > 1 and meta[header_row - 1] and meta[header_row - 1].type == 'blank' then
|
|
start_row = header_row - 1
|
|
end
|
|
local end_row = last_row
|
|
if last_row < #meta and meta[last_row + 1] and meta[last_row + 1].type == 'blank' then
|
|
end_row = last_row + 1
|
|
end
|
|
|
|
vim.cmd('normal! ' .. start_row .. 'GV' .. end_row .. 'G')
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.a_category_visual(count)
|
|
vim.cmd('normal! \27')
|
|
M.a_category(count)
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.i_category(count)
|
|
local _ = count
|
|
local meta = buffer.meta()
|
|
if not meta or #meta == 0 then
|
|
return
|
|
end
|
|
local view = buffer.current_view_name()
|
|
if view == 'priority' then
|
|
return
|
|
end
|
|
|
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
|
local header_row, last_row = M.category_bounds(row, meta)
|
|
if not header_row or not last_row then
|
|
return
|
|
end
|
|
|
|
local first_task = nil
|
|
local last_task = nil
|
|
for r = header_row + 1, last_row do
|
|
if meta[r] and meta[r].type == 'task' then
|
|
if not first_task then
|
|
first_task = r
|
|
end
|
|
last_task = r
|
|
end
|
|
end
|
|
|
|
if not first_task or not last_task then
|
|
return
|
|
end
|
|
|
|
vim.cmd('normal! ' .. first_task .. 'GV' .. last_task .. 'G')
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.i_category_visual(count)
|
|
vim.cmd('normal! \27')
|
|
M.i_category(count)
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.next_header(count)
|
|
local meta = buffer.meta()
|
|
if not meta or #meta == 0 then
|
|
return
|
|
end
|
|
local view = buffer.current_view_name()
|
|
if view == 'priority' then
|
|
return
|
|
end
|
|
|
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
|
local found = 0
|
|
count = math.max(1, count)
|
|
for r = row + 1, #meta do
|
|
if meta[r] and meta[r].type == 'header' then
|
|
found = found + 1
|
|
if found == count then
|
|
vim.api.nvim_win_set_cursor(0, { r, 0 })
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.prev_header(count)
|
|
local meta = buffer.meta()
|
|
if not meta or #meta == 0 then
|
|
return
|
|
end
|
|
local view = buffer.current_view_name()
|
|
if view == 'priority' then
|
|
return
|
|
end
|
|
|
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
|
local found = 0
|
|
count = math.max(1, count)
|
|
for r = row - 1, 1, -1 do
|
|
if meta[r] and meta[r].type == 'header' then
|
|
found = found + 1
|
|
if found == count then
|
|
vim.api.nvim_win_set_cursor(0, { r, 0 })
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.next_task(count)
|
|
local meta = buffer.meta()
|
|
if not meta or #meta == 0 then
|
|
return
|
|
end
|
|
|
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
|
local found = 0
|
|
count = math.max(1, count)
|
|
for r = row + 1, #meta do
|
|
if meta[r] and meta[r].type == 'task' then
|
|
found = found + 1
|
|
if found == count then
|
|
vim.api.nvim_win_set_cursor(0, { r, 0 })
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param count integer
|
|
---@return nil
|
|
function M.prev_task(count)
|
|
local meta = buffer.meta()
|
|
if not meta or #meta == 0 then
|
|
return
|
|
end
|
|
|
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
|
local found = 0
|
|
count = math.max(1, count)
|
|
for r = row - 1, 1, -1 do
|
|
if meta[r] and meta[r].type == 'task' then
|
|
found = found + 1
|
|
if found == count then
|
|
vim.api.nvim_win_set_cursor(0, { r, 0 })
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return M
|