local buffer = require('pending.buffer') local config = require('pending.config') local log = require('pending.log') ---@class pending.textobj local M = {} ---@param ... any ---@return nil local function dbg(...) log.debug(string.format(...)) end ---@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] dbg('next_header: cursor=%d, meta_len=%d, view=%s', row, #meta, view or 'nil') 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 dbg( 'next_header: found header at row=%d, cat=%s, found=%d/%d', r, meta[r].category or '?', found, count ) if found == count then vim.api.nvim_win_set_cursor(0, { r, 0 }) dbg('next_header: cursor set to row=%d, actual=%d', r, vim.api.nvim_win_get_cursor(0)[1]) return end else dbg('next_header: row=%d type=%s', r, meta[r] and meta[r].type or 'nil') end end dbg('next_header: no header found after row=%d', row) 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] dbg('prev_header: cursor=%d, meta_len=%d', row, #meta) 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 dbg( 'prev_header: found header at row=%d, cat=%s, found=%d/%d', r, meta[r].category or '?', found, count ) 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] dbg('next_task: cursor=%d, meta_len=%d', row, #meta) 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 dbg('next_task: jumping to row=%d', r) vim.api.nvim_win_set_cursor(0, { r, 0 }) return end end end dbg('next_task: no task found after row=%d', row) 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] dbg('prev_task: cursor=%d, meta_len=%d', row, #meta) 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 dbg('prev_task: jumping to row=%d', r) vim.api.nvim_win_set_cursor(0, { r, 0 }) return end end end dbg('prev_task: no task found before row=%d', row) end return M