feat(file-token): file: inline metadata token with gf navigation (#45)
* feat(file-token): add file: inline metadata token with gf navigation Problem: there was no way to link a task to a specific location in a source file, or to quickly jump from a task to the relevant code. Solution: add a file:<path>:<line> inline token that stores a relative file reference in task._extra.file. Virtual text renders basename:line in a new PendingFile highlight group. A buffer-local gf mapping (configurable via keymaps.goto_file) opens the file at the given line. M.add_here() lets users attach the current cursor position to any task via vim.ui.select(). M.edit() gains -file support to clear the reference. <Plug>(pending-goto-file) and <Plug>(pending-add-here) are exposed for custom mappings. * test(file-token): add parse, diff, views, edit, and navigation tests Problem: the file: token implementation had no test coverage. Solution: add spec/file_spec.lua covering parse.body extraction, malformed token handling, duplicate token stop-parsing, diff reconciliation (store/update/clear/round-trip), LineMeta population in both views, :Pending edit -file, and goto_file notify paths for no-file and unreadable-file cases. All 292 tests pass. * style: apply stylua formatting * fix(types): remove empty elseif block, fix file? annotation nullability
This commit is contained in:
parent
994294393c
commit
1748e5caa1
10 changed files with 605 additions and 8 deletions
|
|
@ -1,4 +1,5 @@
|
|||
local buffer = require('pending.buffer')
|
||||
local config = require('pending.config')
|
||||
local diff = require('pending.diff')
|
||||
local parse = require('pending.parse')
|
||||
local store = require('pending.store')
|
||||
|
|
@ -305,6 +306,16 @@ function M._setup_buf_mappings(bufnr)
|
|||
end, opts)
|
||||
end
|
||||
end
|
||||
|
||||
local goto_key = km.goto_file
|
||||
if goto_key == nil then
|
||||
goto_key = 'gf'
|
||||
end
|
||||
if goto_key and goto_key ~= false then
|
||||
vim.keymap.set('n', goto_key --[[@as string]], function()
|
||||
M.goto_file()
|
||||
end, opts)
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
|
|
@ -629,6 +640,9 @@ local function parse_edit_token(token)
|
|||
if token == '-rec' or token == '-' .. rk then
|
||||
return 'recur', vim.NIL, nil
|
||||
end
|
||||
if token == '-file' then
|
||||
return 'file_clear', true, nil
|
||||
end
|
||||
|
||||
local due_val = token:match('^' .. vim.pesc(dk) .. ':(.+)$')
|
||||
if due_val then
|
||||
|
|
@ -673,10 +687,11 @@ local function parse_edit_token(token)
|
|||
.. dk
|
||||
.. ':<date>, cat:<name>, '
|
||||
.. rk
|
||||
.. ':<pattern>, +!, -!, -'
|
||||
.. ':<pattern>, file:<path>:<line>, +!, -!, -'
|
||||
.. dk
|
||||
.. ', -cat, -'
|
||||
.. rk
|
||||
.. ', -file'
|
||||
end
|
||||
|
||||
---@param id_str string
|
||||
|
|
@ -755,6 +770,9 @@ function M.edit(id_str, rest)
|
|||
elseif field == 'priority' then
|
||||
updates.priority = value
|
||||
table.insert(feedback, value == 1 and 'priority added' or 'priority removed')
|
||||
elseif field == 'file_clear' then
|
||||
updates.file_clear = true
|
||||
table.insert(feedback, 'file reference removed')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -766,6 +784,18 @@ function M.edit(id_str, rest)
|
|||
end
|
||||
|
||||
store.update(id, updates)
|
||||
|
||||
if updates.file_clear then
|
||||
local t = store.get(id)
|
||||
if t and t._extra then
|
||||
t._extra.file = nil
|
||||
if next(t._extra) == nil then
|
||||
t._extra = nil
|
||||
end
|
||||
t.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
|
||||
end
|
||||
end
|
||||
|
||||
store.save()
|
||||
|
||||
local bufnr = buffer.bufnr()
|
||||
|
|
@ -776,6 +806,90 @@ function M.edit(id_str, rest)
|
|||
vim.notify('Task #' .. id .. ' updated: ' .. table.concat(feedback, ', '))
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.goto_file()
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
if vim.bo[bufnr].filetype ~= 'pending' then
|
||||
return
|
||||
end
|
||||
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local meta = buffer.meta()
|
||||
local m = meta and meta[lnum]
|
||||
if not m or m.type ~= 'task' then
|
||||
vim.notify('No task on this line', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local task = store.get(m.id)
|
||||
if not task or not task._extra or not task._extra.file then
|
||||
vim.notify('No file attached to this task', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local file_spec = task._extra.file
|
||||
local rel_path, line_str = file_spec:match('^(.+):(%d+)$')
|
||||
if not rel_path then
|
||||
vim.notify('Invalid file spec: ' .. file_spec, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local data_dir = vim.fn.fnamemodify(config.get().data_path, ':h')
|
||||
local abs_path = data_dir .. '/' .. rel_path
|
||||
if vim.fn.filereadable(abs_path) == 0 then
|
||||
vim.notify('File not found: ' .. abs_path, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.cmd.edit(abs_path)
|
||||
local lnum_target = tonumber(line_str) or 1
|
||||
vim.api.nvim_win_set_cursor(0, { lnum_target, 0 })
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.add_here()
|
||||
local cur_bufnr = vim.api.nvim_get_current_buf()
|
||||
if vim.bo[cur_bufnr].filetype == 'pending' then
|
||||
vim.notify('Already in pending buffer', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local cur_file = vim.api.nvim_buf_get_name(cur_bufnr)
|
||||
if cur_file == '' or vim.fn.filereadable(cur_file) == 0 then
|
||||
vim.notify('Not editing a readable file', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local cur_lnum = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local data_dir = vim.fn.fnamemodify(config.get().data_path, ':h')
|
||||
local abs_file = vim.fn.fnamemodify(cur_file, ':p')
|
||||
local rel_file
|
||||
if abs_file:sub(1, #data_dir + 1) == data_dir .. '/' then
|
||||
rel_file = abs_file:sub(#data_dir + 2)
|
||||
else
|
||||
rel_file = abs_file
|
||||
end
|
||||
local file_spec = rel_file .. ':' .. cur_lnum
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
if #tasks == 0 then
|
||||
vim.notify('No active tasks', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
local items = {}
|
||||
for _, task in ipairs(tasks) do
|
||||
table.insert(items, task)
|
||||
end
|
||||
vim.ui.select(items, {
|
||||
prompt = 'Attach file to task:',
|
||||
format_item = function(task)
|
||||
return '[' .. task.id .. '] ' .. task.description
|
||||
end,
|
||||
}, function(task)
|
||||
if not task then
|
||||
return
|
||||
end
|
||||
task._extra = task._extra or {}
|
||||
task._extra.file = file_spec
|
||||
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
|
||||
store.save()
|
||||
vim.notify('Attached ' .. file_spec .. ' to task ' .. task.id)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param args string
|
||||
---@return nil
|
||||
function M.command(args)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue