Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 12 additions & 1 deletion .luarc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
"version": "LuaJIT"
},
"workspace": {
"library": ["lua", "$VIMRUNTIME", "${3rd}/luv/library"]
"library": [
"lua",
"$VIMRUNTIME",
"${3rd}/luv/library",
"${3rd}/busted/library",
"${3rd}/luassert/library"
]
},
"diagnostics": {
"globals": [
"vim",
]
}
}
80 changes: 80 additions & 0 deletions lua/tabi/extmarks.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---@class TabiExtmarks
local M = {}

-- Namespace for position-tracking extmarks (separate from the display namespace)
local ns = vim.api.nvim_create_namespace("tabi_positions")

-- Per-buffer mapping: bufnr -> { [note_id] -> { start_id: number, end_id: number } }
local _buf_marks = {}

--- Register extmarks for a note so its position is tracked across edits
---@param bufnr number
---@param note NoteData
function M.track(bufnr, note)
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end

if not _buf_marks[bufnr] then
_buf_marks[bufnr] = {}
end

-- Remove stale extmarks for this note before re-creating
M.untrack(bufnr, note.id)

local line_count = vim.api.nvim_buf_line_count(bufnr)
local start_row = math.min(note.line - 1, line_count - 1)
local end_row = math.min((note.end_line or note.line) - 1, line_count - 1)

local start_id = vim.api.nvim_buf_set_extmark(bufnr, ns, start_row, 0, {})
local end_id = vim.api.nvim_buf_set_extmark(bufnr, ns, end_row, 0, {})

_buf_marks[bufnr][note.id] = { start_id = start_id, end_id = end_id }
end

--- Remove extmarks for a specific note
---@param bufnr number
---@param note_id string
function M.untrack(bufnr, note_id)
if not _buf_marks[bufnr] or not _buf_marks[bufnr][note_id] then
return
end
local ids = _buf_marks[bufnr][note_id]
pcall(vim.api.nvim_buf_del_extmark, bufnr, ns, ids.start_id)
pcall(vim.api.nvim_buf_del_extmark, bufnr, ns, ids.end_id)
_buf_marks[bufnr][note_id] = nil
end

--- Get current positions (1-indexed) for all tracked notes in a buffer
---@param bufnr number
---@return table<string, {line: number, end_line: number}>
function M.get_positions(bufnr)
if not _buf_marks[bufnr] then
return {}
end

local positions = {}
for note_id, ids in pairs(_buf_marks[bufnr]) do
local start_pos = vim.api.nvim_buf_get_extmark_by_id(bufnr, ns, ids.start_id, {})
local end_pos = vim.api.nvim_buf_get_extmark_by_id(bufnr, ns, ids.end_id, {})

if start_pos and #start_pos >= 1 and end_pos and #end_pos >= 1 then
positions[note_id] = {
line = start_pos[1] + 1, -- convert 0-indexed row to 1-indexed line
end_line = end_pos[1] + 1,
}
end
end
return positions
end

--- Clear all extmarks for a buffer and remove it from the tracking table
---@param bufnr number
function M.clear(bufnr)
if vim.api.nvim_buf_is_valid(bufnr) then
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
end
_buf_marks[bufnr] = nil
end

return M
3 changes: 2 additions & 1 deletion lua/tabi/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ function M._setup_default_session_display()
callback = function(args)
-- Only show when no named session or retrace is active
if not M.state.current_session and not require("tabi.retrace").is_active() then
display.update_for_session(args.buf, default_session)
local fresh = session_module.load("default")
display.update_for_session(args.buf, fresh or default_session)
end
end,
})
Expand Down
42 changes: 42 additions & 0 deletions lua/tabi/session.lua
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ end
function M.remove_note(session, note_id)
for i, note in ipairs(session.notes) do
if note.id == note_id then
local bufnr = vim.fn.bufnr(note.file)
if bufnr ~= -1 then
require("tabi.extmarks").untrack(bufnr, note_id)
end
table.remove(session.notes, i)
return M.save(session)
end
Expand Down Expand Up @@ -206,6 +210,44 @@ function M.get_notes_for_file(session, file_path)
return notes
end

--- Sync note positions from extmarks for notes belonging to a specific buffer.
--- Should be called before saving so that line numbers reflect the current state
--- of the buffer after edits.
---@param session SessionData
---@param bufnr number
---@return boolean changed Whether any positions were updated
function M.sync_positions_from_extmarks(session, bufnr)
if not vim.api.nvim_buf_is_valid(bufnr) then
return false
end

local file_path = vim.api.nvim_buf_get_name(bufnr)
if file_path == "" then
return false
end

local extmarks = require("tabi.extmarks")
local positions = extmarks.get_positions(bufnr)
local changed = false

for _, note in ipairs(session.notes) do
if note.file == file_path and positions[note.id] then
local pos = positions[note.id]
if note.line ~= pos.line or note.end_line ~= pos.end_line then
note.line = pos.line
note.end_line = pos.end_line
changed = true
end
end
end

if changed then
M.save(session)
end

return changed
end

--- Get note at specific line
---@param session SessionData
---@param file_path string
Expand Down
25 changes: 24 additions & 1 deletion lua/tabi/ui/display.lua
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,22 @@ end
function M.refresh_buffer(bufnr, notes)
M.clear_buffer(bufnr)

local extmarks = require("tabi.extmarks")

-- Capture positions tracked by extmarks BEFORE clearing them.
-- These reflect any line shifts caused by edits since the last save,
-- so they must take priority over the (possibly stale) positions in the session file.
local tracked_positions = extmarks.get_positions(bufnr)
extmarks.clear(bufnr)

for _, note in ipairs(notes) do
local pos = tracked_positions[note.id]
if pos then
note.line = pos.line
note.end_line = pos.end_line
end
M.display_note_as_virtual_line(bufnr, note)
extmarks.track(bufnr, note)
end
end

Expand Down Expand Up @@ -125,7 +139,8 @@ function M.setup_autocmds(session)
vim.api.nvim_create_autocmd({ "BufEnter", "BufWinEnter" }, {
group = group,
callback = function(args)
M.update_for_session(args.buf, session)
local fresh = require("tabi.session").load(session.id)
M.update_for_session(args.buf, fresh or session)
end,
})

Expand All @@ -137,6 +152,14 @@ function M.setup_autocmds(session)
-- M.clear_buffer(args.buf)
end,
})

-- Clean up extmarks when a buffer is deleted
vim.api.nvim_create_autocmd("BufDelete", {
group = group,
callback = function(args)
require("tabi.extmarks").clear(args.buf)
end,
})
end

--- Clear all autocommands
Expand Down
21 changes: 21 additions & 0 deletions plugin/tabi.lua
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,27 @@ function commands.session_rename(args)
end
end

-- Sync note positions from extmarks just before the buffer is written to disk
vim.api.nvim_create_autocmd("BufWritePre", {
group = vim.api.nvim_create_augroup("TabiPositionSync", { clear = true }),
callback = function(args)
-- Prefer the explicitly active session; fall back to the default session
-- when show_default_notes is displaying notes without an active session
-- (e.g. after a Neovim restart).
local session_id = tabi.state.current_session
if not session_id then
if not require("tabi.config").get().show_default_notes then
return
end
session_id = "default"
end
local session = session_module.load(session_id)
if session then
session_module.sync_positions_from_extmarks(session, args.buf)
end
end,
})

-- Create user commands
vim.api.nvim_create_user_command("Tabi", function(opts)
local args = vim.split(vim.trim(opts.args), "%s+")
Expand Down
Loading
Loading