Files
eleith c1b4eafd55
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
Recognize reopened manager windows without adopting location lists
2026-09-30 09:31:12 -07:00

1018 lines
37 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
local clipboard = require("scratch_comments.export.clipboard")
local helpers = require("helpers")
local scratch = require("scratch_comments")
local store = require("scratch_comments.model.store")
local expect = MiniTest.expect
local copy = clipboard.copy
local copied
local source
before_each(function()
helpers.reset()
copied = nil
---@diagnostic disable-next-line: duplicate-set-field -- test fake
clipboard.copy = function(text)
copied = text
return true
end
source = helpers.buffer("export-fixture.lua", { "local a = 1", "local b = 2", "local c = 3" })
vim.cmd("3Comment")
helpers.write("on c")
vim.cmd("1Comment")
helpers.write("on a")
end)
after_each(function()
clipboard.copy = copy
end)
describe("json", function()
it("lists every comment in position order, with its lines and text", function()
local data = vim.json.decode(scratch.render("json"))
expect.equality(#data.comments, 2)
expect.equality(data.comments[1].comment, "on a")
expect.equality(data.comments[1].start_line, 1)
expect.equality(data.comments[1].snippet, "local a = 1")
end)
it("always has an orphaned list, encoded as an array", function()
expect.equality(#vim.json.decode(scratch.render("json")).orphaned, 0)
expect.no_equality(scratch.render("json"):find('"orphaned": []', 1, true), nil)
end)
end)
describe(":CommentExport", function()
it("copies markdown by default, keeping the comments", function()
vim.cmd("CommentExport")
expect.no_equality(copied:find("## export-fixture.lua", 1, true), nil)
expect.no_equality(copied:find("on a", 1, true), nil)
expect.equality(#vim.json.decode(scratch.render("json")).comments, 2)
end)
it("copies json", function()
vim.cmd("CommentExport json")
expect.equality(vim.json.decode(copied).comments[2].comment, "on c")
end)
it("says so when the clipboard can't be written", function()
---@diagnostic disable-next-line: duplicate-set-field -- test fake
clipboard.copy = function()
return false
end
vim.cmd("CommentExport")
expect.equality(
helpers.notified()[#helpers.notified()],
"Could not copy to the clipboard; no provider configured?"
)
end)
it("copies nothing for an unknown format", function()
vim.cmd("CommentExport bogus")
expect.equality(copied, nil)
end)
end)
describe(":CommentExport!", function()
it("opens markdown in a scratch buffer by default", function()
vim.cmd("CommentExport!")
expect.equality(vim.bo.buftype, "nofile")
expect.equality(vim.bo.filetype, "markdown")
expect.no_equality(helpers.text():find("on a", 1, true), nil)
end)
it("opens json when asked", function()
vim.cmd("CommentExport! json")
expect.equality(vim.bo.filetype, "json")
end)
it("returns to the file when the scratch window closes", function()
vim.cmd("CommentExport!")
vim.cmd("close")
expect.equality(vim.api.nvim_get_current_buf(), source)
end)
end)
describe(":CommentList", function()
it("fills a titled quickfix list with one entry per comment", function()
vim.cmd("CommentList")
local qf = vim.fn.getqflist({ title = 1, items = 1 })
expect.equality(qf.title, "Comments")
expect.equality(#qf.items, 2)
expect.equality(qf.items[1].text, "on a")
vim.cmd("cclose")
end)
end)
describe("the comment list manager", function()
it("edits a comment in the same card, then refreshes the list", function()
vim.cmd("CommentList")
local id = vim.fn.getqflist()[1].user_data.scratch_comments_id
vim.cmd.Comment()
expect.equality(helpers.text(), "on a")
expect.equality(vim.bo.modifiable, true)
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "revised" })
vim.cmd.write()
expect.equality(vim.fn.getqflist()[1].text, "revised")
vim.cmd.quit()
vim.wait(0)
expect.equality(vim.bo.buftype, "quickfix")
expect.equality(vim.fn.getqflist()[1].text, "revised")
expect.equality(vim.fn.getqflist()[1].user_data.scratch_comments_id, id)
vim.cmd.cclose()
end)
it("doesn't reopen a list the user closed while editing", function()
vim.cmd.CommentList()
vim.cmd.Comment()
vim.cmd.cclose()
vim.cmd.quit()
vim.wait(0)
expect.equality(vim.fn.getqflist({ winid = 0 }).winid, 0)
end)
it("leaves the source cursor alone while browsing", function()
vim.api.nvim_win_set_cursor(0, { 1, 0 })
local source_win = vim.api.nvim_get_current_win()
vim.cmd.CommentList()
vim.api.nvim_win_set_cursor(0, { 2, 0 })
vim.api.nvim_exec_autocmds("CursorMoved", { buffer = 0 })
expect.equality(vim.api.nvim_win_get_cursor(source_win)[1], 1)
vim.cmd("normal \r")
expect.equality(vim.api.nvim_win_get_cursor(source_win)[1], 3)
expect.equality(#helpers.every_float(), 0)
end)
it("doesn't return an orphan editor to another plugin's quickfix list", function()
vim.cmd("1d")
helpers.text_changed()
local file_win = vim.api.nvim_get_current_win()
vim.cmd.CommentList()
vim.api.nvim_win_set_cursor(0, { 2, 0 })
vim.cmd.Comment()
vim.fn.setqflist({}, " ", { items = { { filename = "other.lua", lnum = 1, text = "other" } } })
vim.cmd.quit()
expect.equality(vim.api.nvim_get_current_win(), file_win)
vim.wait(0)
expect.equality(vim.api.nvim_get_current_win(), file_win)
end)
it("does not pull focus back from another tab after closing a card", function()
vim.cmd.CommentList()
vim.cmd.Comment()
local card_win = vim.api.nvim_get_current_win()
vim.cmd.tabnew()
local other_tab = vim.api.nvim_get_current_tabpage()
vim.api.nvim_set_current_win(card_win)
vim.cmd("quit | tabnext")
vim.wait(0)
expect.equality(vim.api.nvim_get_current_tabpage(), other_tab)
end)
it("keeps focus on an export replacing a manager-opened card", function()
vim.cmd.CommentList()
vim.cmd.Comment()
vim.cmd("CommentExport!")
vim.wait(0)
expect.equality(vim.bo.buftype, "nofile")
expect.equality(vim.bo.filetype, "markdown")
vim.cmd.cclose()
end)
it("edits a comment whose source was wiped", function()
vim.cmd("bwipeout! " .. source)
vim.cmd.CommentList()
vim.cmd.Comment()
expect.equality(helpers.text(), "on a")
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "revised parked" })
vim.cmd("wq")
vim.wait(0)
expect.equality(vim.fn.getqflist()[1].text, "revised parked")
expect.equality(store.all()[2].state, "parked")
vim.cmd.cclose()
end)
it("does not guess a jump for an orphan; :Comment opens its card", function()
vim.cmd("1d")
helpers.text_changed()
vim.cmd.CommentList()
vim.api.nvim_win_set_cursor(0, { 2, 0 })
vim.api.nvim_exec_autocmds("CursorMoved", { buffer = 0 })
local qf_win = vim.api.nvim_get_current_win()
vim.cmd("normal \r")
expect.equality(vim.api.nvim_get_current_win(), qf_win)
expect.equality(#helpers.every_float(), 0)
expect.equality(
helpers.notified()[#helpers.notified()],
"This comment has no source location; use :Comment to edit it"
)
vim.cmd.Comment()
expect.equality(helpers.text(), "on a")
local dim = vim.iter(helpers.every_float()):find(function(win)
return vim.w[win].scratch_comments_backdrop
end)
local parent = assert(vim.api.nvim_win_get_config(dim).win)
expect.equality(vim.api.nvim_win_get_buf(parent), source)
expect.no_equality(parent, qf_win)
vim.cmd.quit()
vim.wait(0)
expect.equality(vim.api.nvim_get_current_win(), qf_win)
expect.equality(vim.bo.buftype, "quickfix")
end)
it("places an orphan card over a file, not an export scratch split", function()
vim.cmd("1d")
helpers.text_changed()
vim.cmd("CommentExport!")
expect.equality(vim.bo.buftype, "nofile")
vim.cmd.CommentList()
vim.api.nvim_win_set_cursor(0, { 2, 0 })
vim.cmd.Comment()
local dim = vim.iter(helpers.every_float()):find(function(win)
return vim.w[win].scratch_comments_backdrop
end)
local parent = assert(vim.api.nvim_win_get_config(dim).win)
expect.equality(vim.api.nvim_win_get_buf(parent), source)
end)
it("makes a file area if only quickfix remains for an orphan", function()
vim.cmd("1d")
helpers.text_changed()
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
local file_win = vim.fn.bufwinid(vim.api.nvim_buf_get_name(source))
vim.api.nvim_win_close(file_win, true)
vim.api.nvim_win_set_cursor(qf_win, { 2, 0 })
vim.cmd.Comment()
local dim = vim.iter(helpers.every_float()):find(function(win)
return vim.w[win].scratch_comments_backdrop
end)
local parent = assert(vim.api.nvim_win_get_config(dim).win)
expect.no_equality(parent, qf_win)
expect.equality(vim.api.nvim_win_is_valid(qf_win), true)
vim.cmd.quit()
vim.wait(0)
expect.equality(vim.api.nvim_get_current_win(), qf_win)
end)
it("keeps quickfix usable if there is no room to open an orphan card", function()
vim.cmd("1d")
helpers.text_changed()
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_close(vim.fn.bufwinid(vim.api.nvim_buf_get_name(source)), true)
local min_height, preferred = vim.o.winminheight, vim.o.winheight
local ok = pcall(function()
vim.o.winheight = 12
vim.o.winminheight = 12
vim.api.nvim_win_set_cursor(qf_win, { 2, 0 })
vim.cmd.Comment()
end)
vim.o.winminheight = min_height
vim.o.winheight = preferred
expect.equality(ok, true)
expect.equality(vim.api.nvim_get_current_win(), qf_win)
expect.equality(#helpers.floats(), 0)
expect.equality(
helpers.notified()[#helpers.notified()],
"Make room for an editing window to open this comment"
)
end)
it("deletes an orphan by saving its card empty", function()
vim.api.nvim_set_current_buf(source)
vim.cmd("1d")
helpers.text_changed()
vim.cmd.CommentList()
vim.api.nvim_win_set_cursor(0, { 2, 0 })
vim.api.nvim_exec_autocmds("CursorMoved", { buffer = 0 })
vim.cmd.Comment()
expect.equality(helpers.text(), "on a")
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "" })
vim.cmd.write()
vim.wait(0)
expect.equality(#store.all(), 1)
expect.equality(vim.bo.buftype, "quickfix")
expect.equality(#vim.fn.getqflist(), 1)
vim.cmd.cclose()
end)
it("removes edited comments that no longer match the active filter", function()
for _, comment in ipairs(store.all()) do
store.update(comment.id, { comment = "ZZQX9 " .. comment.comment })
end
vim.cmd("CommentList ZZQX9")
local removed = vim.fn.getqflist()[1].user_data.scratch_comments_id
vim.cmd.Comment()
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "changed" })
vim.cmd.write()
local items = vim.fn.getqflist()
expect.equality(#items, 1)
expect.equality(items[1].text, "ZZQX9 on c")
expect.no_equality(items[1].user_data.scratch_comments_id, removed)
end)
it("deletes the opened comment and keeps the filtered list", function()
vim.cmd("CommentList export-fixture")
local removed = vim.fn.getqflist()[1].user_data.scratch_comments_id
vim.cmd.Comment()
vim.cmd.CommentDelete()
expect.equality(#store.all(), 1)
expect.no_equality(store.all()[1].id, removed)
expect.equality(#vim.fn.getqflist(), 1)
vim.cmd.cclose()
end)
it("lists and exports comments after their source was wiped", function()
vim.cmd("bwipeout! " .. source)
local data = vim.json.decode(scratch.render("json"))
expect.equality(#data.comments, 2)
expect.equality(data.comments[1].snippet, "local a = 1")
vim.cmd.CommentList()
expect.equality(#vim.fn.getqflist(), 2)
vim.cmd.cclose()
end)
it("deletes a parked comment after opening its card", function()
vim.cmd("bwipeout! " .. source)
expect.equality(#store.all(), 2)
vim.cmd.CommentList()
vim.cmd.Comment()
vim.cmd.CommentDelete()
expect.equality(#store.all(), 1)
expect.equality(store.all()[1].comment, "on c")
vim.cmd.cclose()
end)
it("deletes a filtered orphan directly from quickfix", function()
vim.cmd("1d")
helpers.text_changed()
vim.cmd("CommentList 1")
local qf_win = vim.api.nvim_get_current_win()
expect.equality(#vim.fn.getqflist(), 1)
expect.equality(
vim.fn.getqflist()[1].user_data.scratch_comments_id,
vim.iter(store.all()):find(function(comment)
return comment.comment == "on a"
end).id
)
vim.cmd.CommentDelete()
expect.equality(#store.all(), 1)
expect.equality(store.all()[1].comment, "on c")
expect.equality(vim.api.nvim_win_is_valid(qf_win), false)
end)
it("deletes an inactive comment from the list", function()
vim.cmd.cclose()
vim.api.nvim_set_current_buf(source)
vim.cmd("1d")
helpers.text_changed()
vim.cmd.CommentList()
local qf = vim.fn.getqflist()
local index = vim.iter(qf):enumerate():find(function(_, item)
return item.text:find("[orphaned]", 1, true) ~= nil
end)
vim.api.nvim_win_set_cursor(0, { index, 0 })
vim.cmd.CommentDelete()
expect.equality(#store.all(), 1)
expect.equality(store.all()[1].comment, "on c")
vim.cmd.cclose()
end)
it("uses normal <CR> navigation when another quickfix list replaces ours", function()
local other = helpers.buffer("other-qf.lua", { "first", "second" })
vim.cmd.CommentList()
vim.fn.setqflist({}, " ", {
items = {
{ bufnr = other, lnum = 1, text = "first" },
{ bufnr = other, lnum = 2, text = "second" },
},
})
vim.api.nvim_win_set_cursor(0, { 2, 0 })
vim.cmd("normal \r")
expect.equality(vim.api.nvim_get_current_buf(), other)
expect.equality(vim.api.nvim_win_get_cursor(0)[1], 2)
expect.equality(#helpers.every_float(), 0)
end)
it("does not adopt a location list after the manager window closes", function()
vim.cmd.CommentList()
vim.cmd.cclose()
vim.fn.setloclist(0, { { bufnr = source, lnum = 1, text = "location" } }, " ")
vim.cmd.lopen()
vim.cmd.CommentDelete()
expect.equality(#store.all(), 2)
expect.equality(helpers.notified()[#helpers.notified()], "Not a Scratch Comments list")
end)
it("does not open a card from another plugin's quickfix list", function()
vim.cmd.CommentList()
vim.fn.setqflist({}, " ", { items = { { filename = "other.lua", lnum = 1, text = "other" } } })
vim.cmd.Comment()
expect.equality(#helpers.every_float(), 0)
expect.equality(helpers.notified()[#helpers.notified()], "Not a Scratch Comments list")
vim.cmd.cclose()
end)
it("does not open a card for a stale quickfix row", function()
vim.cmd.CommentList()
local items = vim.fn.getqflist()
items[1].user_data = {}
vim.fn.setqflist({}, "r", { items = items })
vim.cmd.Comment()
expect.equality(#helpers.every_float(), 0)
expect.equality(helpers.notified()[#helpers.notified()], "This comment is gone")
vim.cmd.cclose()
end)
it("does not delete another plugin's quickfix entry", function()
vim.cmd.CommentList()
vim.fn.setqflist({}, " ", { items = { { filename = "other.lua", lnum = 1, text = "other" } } })
vim.cmd.CommentDelete()
expect.equality(#store.all(), 2)
vim.cmd.cclose()
end)
it("jumps to a parked URI after a delayed read", function()
local url = "review-test:///comments.lua"
local pending
vim.api.nvim_create_autocmd("BufReadCmd", {
pattern = "review-test:///*",
once = true,
callback = function(args)
pending = args.buf
end,
})
vim.cmd("enew!")
vim.api.nvim_buf_set_name(0, url)
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "first", "second" })
vim.bo.bufhidden = "wipe"
vim.cmd("2Comment")
helpers.write("URI comment")
local id = store.all()[#store.all()].id
vim.cmd("enew!")
vim.cmd.CommentList()
local index
for i, item in ipairs(vim.fn.getqflist()) do
if item.user_data.scratch_comments_id == id then
index = i
end
end
vim.api.nvim_win_set_cursor(0, { index, 0 })
vim.cmd("normal \r")
local source_win = vim.fn.bufwinid(url)
expect.equality(vim.api.nvim_win_get_buf(source_win), pending)
expect.equality(store.all()[#store.all()].state, "parked")
vim.api.nvim_buf_set_lines(pending, 0, -1, false, { "first", "second" })
vim.wait(0)
expect.equality(store.all()[#store.all()].state, nil)
expect.equality(vim.api.nvim_win_get_cursor(source_win)[1], 2)
vim.cmd.quit()
vim.wait(0)
vim.api.nvim_win_set_cursor(0, { 1, 0 })
vim.api.nvim_buf_set_lines(pending, 0, 1, false, { "updated" })
vim.wait(0)
expect.equality(vim.api.nvim_win_get_cursor(0)[1], 1)
end)
it("returns to quickfix after an asynchronous source becomes an orphan", function()
local url = "review-test:///orphan.lua"
local pending
vim.api.nvim_create_autocmd("BufReadCmd", {
pattern = "review-test:///*",
once = true,
callback = function(args)
pending = args.buf
end,
})
vim.cmd.enew()
vim.api.nvim_buf_set_name(0, url)
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "first", "second" })
vim.bo.bufhidden = "wipe"
vim.cmd("2Comment")
helpers.write("async orphan")
local id = store.all()[#store.all()].id
vim.cmd("enew!")
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
local index = vim.iter(vim.fn.getqflist()):enumerate():find(function(_, item)
return item.user_data.scratch_comments_id == id
end)
vim.api.nvim_win_set_cursor(qf_win, { index, 0 })
vim.cmd("normal \r")
vim.api.nvim_buf_set_lines(pending, 0, -1, false, { "first", "changed" })
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = pending })
vim.wait(0)
expect.equality(store.all()[#store.all()].state, "inactive")
expect.equality(vim.api.nvim_get_current_win(), qf_win)
expect.equality(vim.fn.getqflist()[index].text, "[orphaned] async orphan")
expect.equality(#helpers.every_float(), 0)
end)
local function jump_to_pending_source(url)
local pending
vim.api.nvim_create_autocmd("BufReadCmd", {
pattern = "review-test:///*",
once = true,
callback = function(args)
pending = args.buf
end,
})
vim.cmd("enew!")
vim.api.nvim_buf_set_name(0, url)
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "first", "second" })
vim.bo.bufhidden = "wipe"
vim.cmd("2Comment")
helpers.write("async orphan")
vim.cmd("enew!")
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
local id = store.all()[#store.all()].id
local index = vim.iter(vim.fn.getqflist()):enumerate():find(function(_, item)
return item.user_data.scratch_comments_id == id
end)
vim.api.nvim_win_set_cursor(qf_win, { index, 0 })
vim.cmd("normal \r")
return pending, qf_win
end
it("does not return to a closed list when a delayed read invalidates a location", function()
local pending, qf_win = jump_to_pending_source("review-test:///closed-list.lua")
local source_win = vim.api.nvim_get_current_win()
vim.cmd.cclose()
vim.api.nvim_buf_set_lines(pending, 0, -1, false, { "first", "changed" })
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = pending })
vim.wait(0)
expect.equality(store.all()[#store.all()].state, "inactive")
expect.equality(vim.api.nvim_win_is_valid(qf_win), false)
expect.equality(vim.api.nvim_get_current_win(), source_win)
end)
it(
"does not enter a replacement quickfix list when a delayed read invalidates a location",
function()
local pending, qf_win = jump_to_pending_source("review-test:///replaced-list.lua")
local source_win = vim.api.nvim_get_current_win()
vim.fn.setqflist(
{},
" ",
{ items = { { filename = "other.lua", lnum = 1, text = "other" } } }
)
vim.api.nvim_buf_set_lines(pending, 0, -1, false, { "first", "changed" })
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = pending })
vim.wait(0)
expect.equality(store.all()[#store.all()].state, "inactive")
expect.equality(vim.api.nvim_get_current_win(), source_win)
expect.equality(vim.api.nvim_win_is_valid(qf_win), true)
expect.equality(vim.fn.getqflist()[1].text, "other")
end
)
it("refreshes the list after leaving a delayed jump that becomes an orphan", function()
local pending, qf_win = jump_to_pending_source("review-test:///left-source.lua")
vim.api.nvim_set_current_win(qf_win)
vim.api.nvim_buf_set_lines(pending, 0, -1, false, { "first", "changed" })
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = pending })
vim.wait(0)
expect.equality(store.all()[#store.all()].state, "inactive")
expect.equality(vim.api.nvim_get_current_win(), qf_win)
expect.equality(vim.fn.getqflist()[#vim.fn.getqflist()].text, "[orphaned] async orphan")
end)
it("doesn't move the source cursor after leaving an asynchronous jump", function()
local url = "review-test:///late-cursor.lua"
local pending
vim.api.nvim_create_autocmd("BufReadCmd", {
pattern = "review-test:///*",
once = true,
callback = function(args)
pending = args.buf
end,
})
vim.cmd.enew()
vim.api.nvim_buf_set_name(0, url)
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "first", "second" })
vim.bo.bufhidden = "wipe"
vim.cmd("2Comment")
helpers.write("late")
vim.cmd("enew!")
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
local index = vim.iter(vim.fn.getqflist()):enumerate():find(function(_, item)
return item.user_data.scratch_comments_id == store.all()[#store.all()].id
end)
vim.api.nvim_win_set_cursor(0, { index, 0 })
vim.cmd("normal \r")
local editor_win = vim.api.nvim_get_current_win()
local source_win = vim.fn.bufwinid(url)
vim.api.nvim_win_set_cursor(source_win, { 1, 0 })
vim.api.nvim_set_current_win(qf_win)
vim.api.nvim_set_current_win(editor_win)
vim.api.nvim_buf_set_lines(pending, 0, -1, false, { "first", "second" })
vim.wait(0)
expect.equality(vim.api.nvim_win_get_cursor(source_win)[1], 1)
end)
it("does not navigate to stale source lines after a mismatched reload", function()
vim.cmd.edit("tests/fixtures/lifecycle.txt")
vim.api.nvim_buf_set_lines(0, 0, 1, false, { "unsaved source" })
vim.cmd.Comment()
helpers.write("comment on unsaved source")
local wanted = store.all()[#store.all()].id
vim.cmd("bdelete!")
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
local index = vim.iter(vim.fn.getqflist()):enumerate():find(function(_, item)
return item.user_data.scratch_comments_id == wanted
end)
vim.api.nvim_win_set_cursor(0, { index, 0 })
vim.cmd("normal \r")
vim.wait(0)
expect.equality(vim.api.nvim_get_current_win(), qf_win)
expect.equality(#helpers.every_float(), 0)
expect.equality(vim.fn.getqflist()[index].text:find("[orphaned]", 1, true), 1)
vim.cmd.Comment()
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "unsaved edit" })
vim.wait(0)
expect.equality(store.all()[#store.all()].state, "inactive")
local context_win = helpers.panes()
expect.equality(
helpers.text(vim.api.nvim_win_get_buf(context_win)),
"The lines this comment was on are gone."
)
expect.equality(helpers.text(), "unsaved edit")
expect.equality(vim.bo.modified, true)
vim.cmd("q!")
vim.wait(0)
expect.equality(vim.api.nvim_get_current_win(), qf_win)
end)
it("jumps to a parked file and restores its comment", function()
vim.cmd("edit! tests/fixtures/lifecycle.txt")
vim.cmd.Comment()
helpers.write("in a real file")
local wanted = store.all()[#store.all()].id
vim.cmd("bdelete")
vim.cmd.CommentList()
local index
for i, item in ipairs(vim.fn.getqflist()) do
if item.user_data.scratch_comments_id == wanted then
index = i
end
end
vim.api.nvim_win_set_cursor(0, { index, 0 })
vim.cmd("normal \r")
vim.wait(0)
expect.equality(vim.api.nvim_buf_get_name(0), store.all()[#store.all()].source_name)
expect.equality(#helpers.every_float(), 0)
expect.equality(store.all()[#store.all()].state, nil)
end)
end)
describe("markdown", function()
it("names a range's lines", function()
vim.cmd("1,2Comment")
helpers.write("on a and b")
expect.no_equality(scratch.render():find("lines 1–2", 1, true), nil)
end)
end)
describe(":CommentList with a filter", function()
---@return string[]
local function listed()
return vim.tbl_map(function(item)
return item.text
end, vim.fn.getqflist())
end
it("keeps only the comments that match", function()
vim.cmd("CommentList 3")
expect.equality(listed(), { "on c" })
vim.cmd("cclose")
end)
it("matches the snippet and the path too", function()
vim.cmd("CommentList local")
expect.equality(#listed(), 2)
vim.cmd("cclose")
vim.cmd("CommentList fixture")
expect.equality(#listed(), 2)
vim.cmd("cclose")
end)
it("matches loosely, like a fuzzy finder", function()
vim.cmd("CommentList lc3")
expect.equality(listed(), { "on c" })
vim.cmd("cclose")
end)
it("keeps the list in file and line order", function()
helpers.buffer("a-first.md", { "alpha" })
vim.cmd("Comment")
helpers.write("also mentions local")
vim.cmd("CommentList local")
expect.equality(listed(), { "also mentions local", "on a", "on c" })
vim.cmd("cclose")
end)
it("says so when nothing matches", function()
vim.cmd("CommentList zzz")
expect.equality(helpers.notified()[#helpers.notified()], "No comments")
end)
end)
describe("one card at a time", function()
it(":CommentList closes an open comment window without opening another", function()
vim.cmd.CommentList()
vim.cmd.Comment()
expect.equality(#helpers.floats(), 2)
vim.cmd("CommentList")
expect.equality(#helpers.floats(), 0)
expect.equality(vim.bo.buftype, "quickfix")
end)
it(":CommentList keeps an unsaved comment, and says so", function()
vim.cmd.CommentList()
vim.cmd.Comment()
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "unsaved" })
vim.cmd("CommentList")
expect.equality(helpers.text(), "unsaved")
expect.equality(helpers.notified()[#helpers.notified()], "Save or discard the comment first")
end)
it(":Comment keeps unsaved edits when selecting another row", function()
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
vim.cmd.Comment()
local card_win = vim.api.nvim_get_current_win()
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "unsaved" })
vim.api.nvim_set_current_win(qf_win)
vim.api.nvim_win_set_cursor(qf_win, { 2, 0 })
vim.cmd.Comment()
expect.equality(#helpers.floats(), 2)
expect.equality(helpers.notified()[#helpers.notified()], "Save or discard the comment first")
expect.equality(helpers.text(vim.api.nvim_win_get_buf(card_win)), "unsaved")
vim.api.nvim_set_current_win(card_win)
vim.cmd("q!")
end)
it(":CommentExport! closes the comment window first", function()
vim.cmd.CommentList()
vim.cmd.Comment()
vim.cmd("CommentExport!")
expect.equality(#helpers.every_float(), 0)
expect.equality(vim.bo.buftype, "nofile")
end)
it("opens a new comment even with the list visible", function()
vim.cmd("CommentList")
vim.cmd("wincmd k")
vim.cmd("2Comment")
expect.equality(#helpers.floats(), 2)
helpers.write("on b")
expect.equality(#vim.fn.getqflist(), 3)
expect.equality(vim.fn.getqflist()[2].text, "on b")
end)
end)
describe("quickfix navigation", function()
it("moves through entries without opening a card or moving the source cursor", function()
vim.api.nvim_win_set_cursor(0, { 1, 0 })
local source_win = vim.api.nvim_get_current_win()
vim.cmd.CommentList()
vim.cmd("normal j")
expect.equality(vim.api.nvim_win_get_cursor(0)[1], 2)
expect.equality(vim.fn.getqflist({ idx = 0 }).idx, 1)
expect.equality(vim.api.nvim_win_get_cursor(source_win)[1], 1)
expect.equality(#helpers.every_float(), 0)
end)
it("jumps on <CR> without opening a card", function()
vim.cmd.CommentList()
vim.cmd("normal j")
vim.cmd("normal \r")
expect.equality(vim.api.nvim_get_current_buf(), source)
expect.equality(vim.api.nvim_win_get_cursor(0)[1], 3)
expect.equality(#helpers.every_float(), 0)
end)
it("keeps the selected quickfix row when saving a card opened from source", function()
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_cursor(qf_win, { 2, 0 })
vim.cmd("wincmd k")
vim.cmd.Comment()
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "revised from source" })
vim.cmd.write()
expect.equality(vim.api.nvim_win_get_cursor(qf_win)[1], 2)
expect.equality(vim.fn.getqflist()[1].text, "revised from source")
end)
it("refreshes the manager after its quickfix window is reopened", function()
vim.cmd.CommentList()
local old_win = vim.api.nvim_get_current_win()
vim.cmd.cclose()
vim.cmd.copen()
local qf_win = vim.api.nvim_get_current_win()
expect.no_equality(qf_win, old_win)
expect.equality(vim.fn.maparg("<CR>", "n", false, true).desc, "Jump to comment source")
vim.cmd("wincmd k")
vim.cmd.Comment()
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "edited after reopening" })
vim.cmd.write()
expect.equality(vim.fn.getqflist()[1].text, "edited after reopening")
expect.equality(vim.api.nvim_win_is_valid(qf_win), true)
end)
it("restores manager mappings on a recreated quickfix buffer", function()
vim.cmd.CommentList()
local qf_buf = vim.api.nvim_get_current_buf()
vim.cmd.cclose()
vim.cmd("bwipeout! " .. qf_buf)
vim.cmd.copen()
expect.no_equality(vim.api.nvim_get_current_buf(), qf_buf)
expect.equality(vim.fn.maparg("<CR>", "n", false, true).desc, "Jump to comment source")
end)
it("refreshes a list in another tab after editing from source", function()
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_cursor(qf_win, { 2, 0 })
vim.cmd.tabnew()
vim.cmd.buffer(source)
vim.cmd.Comment()
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "edited in tab two" })
local editing_tab, editing_win =
vim.api.nvim_get_current_tabpage(), vim.api.nvim_get_current_win()
vim.cmd.write()
expect.equality(vim.fn.getqflist()[1].text, "edited in tab two")
expect.equality(vim.api.nvim_get_current_tabpage(), editing_tab)
expect.equality(vim.api.nvim_get_current_win(), editing_win)
expect.equality(vim.api.nvim_win_get_cursor(qf_win)[1], 2)
end)
it("refreshes a list in another tab after deleting from source", function()
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
vim.cmd.tabnew()
vim.cmd.buffer(source)
vim.cmd.Comment()
local editing_tab, editing_win =
vim.api.nvim_get_current_tabpage(), vim.api.nvim_get_current_win()
vim.cmd.CommentDelete()
expect.equality(vim.api.nvim_get_current_tabpage(), editing_tab)
expect.equality(vim.api.nvim_get_current_win() ~= qf_win, true)
expect.equality(vim.api.nvim_win_is_valid(editing_win), false)
expect.equality(#vim.fn.getqflist(), 1)
expect.equality(vim.fn.getqflist()[1].text, "on c")
expect.equality(vim.api.nvim_win_is_valid(qf_win), true)
end)
it("closes an empty filtered manager in another tab without taking focus", function()
store.update(store.all()[2].id, { comment = "ZZQX9 only" })
vim.cmd("CommentList ZZQX9")
local qf_win = vim.api.nvim_get_current_win()
vim.cmd.tabnew()
vim.cmd.buffer(source)
vim.api.nvim_win_set_cursor(0, { 1, 0 })
vim.cmd.Comment()
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "no longer matches" })
local editing_tab = vim.api.nvim_get_current_tabpage()
vim.cmd.write()
expect.equality(vim.api.nvim_get_current_tabpage(), editing_tab)
expect.equality(vim.api.nvim_win_is_valid(qf_win), false)
end)
it("refreshes the list when editing from the source after a jump", function()
vim.cmd.CommentList()
vim.cmd("normal \r")
expect.equality(#helpers.every_float(), 0)
vim.cmd.Comment()
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "revised from source" })
vim.cmd.write()
expect.equality(vim.fn.getqflist()[1].text, "revised from source")
vim.cmd.quit()
end)
it(":Comment edits the cursor row without moving the source cursor", function()
local source_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_cursor(source_win, { 1, 0 })
vim.cmd.CommentList()
vim.cmd("normal j")
vim.cmd.Comment()
expect.equality(helpers.text(), "on c")
expect.equality(vim.api.nvim_win_get_cursor(source_win)[1], 1)
local dim = vim.iter(helpers.every_float()):find(function(win)
return vim.w[win].scratch_comments_backdrop
end)
local config = vim.api.nvim_win_get_config(dim)
expect.equality(config.relative, "win")
expect.equality(config.win, source_win)
expect.equality(config.height, vim.api.nvim_win_get_height(source_win))
local qf_win = vim.fn.getqflist({ winid = 0 }).winid
expect.equality(
vim.fn.win_screenpos(source_win)[1] + config.height <= vim.fn.win_screenpos(qf_win)[1],
true
)
vim.cmd.quit()
vim.wait(0)
expect.equality(vim.api.nvim_get_current_win(), qf_win)
expect.equality(vim.api.nvim_win_get_cursor(source_win)[1], 1)
end)
it("does not jump away from unsaved card edits", function()
local source_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_cursor(source_win, { 1, 0 })
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
vim.cmd.Comment()
local card_win = vim.api.nvim_get_current_win()
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "unsaved" })
vim.api.nvim_set_current_win(qf_win)
vim.api.nvim_win_set_cursor(qf_win, { 2, 0 })
vim.cmd("normal \r")
expect.equality(vim.api.nvim_get_current_win(), qf_win)
expect.equality(vim.api.nvim_win_get_cursor(source_win)[1], 1)
expect.equality(helpers.text(vim.api.nvim_win_get_buf(card_win)), "unsaved")
expect.equality(helpers.notified()[#helpers.notified()], "Save or discard the comment first")
vim.api.nvim_set_current_win(card_win)
vim.cmd("q!")
end)
it("keeps another editing split visible behind the card", function()
vim.cmd.vsplit()
vim.cmd.CommentList()
vim.cmd.Comment()
local dim = vim.iter(helpers.every_float()):find(function(win)
return vim.w[win].scratch_comments_backdrop
end)
local config = vim.api.nvim_win_get_config(dim)
expect.equality(config.relative, "win")
expect.equality(config.width < vim.o.columns, true)
expect.equality(vim.api.nvim_win_get_buf(config.win), source)
end)
it("has nothing to dismiss on <Esc> before opening a card", function()
vim.cmd.CommentList()
vim.cmd([[execute "normal \<Esc>"]])
expect.equality(vim.bo.buftype, "quickfix")
expect.equality(#helpers.every_float(), 0)
end)
it("closes a clean editor on <Esc> from the list, but preserves unsaved edits", function()
vim.cmd.CommentList()
local qf_win = vim.api.nvim_get_current_win()
vim.cmd.Comment()
local comment_buf = vim.api.nvim_get_current_buf()
vim.api.nvim_set_current_win(qf_win)
vim.api.nvim_buf_set_lines(comment_buf, 0, -1, false, { "unsaved" })
vim.cmd([[execute "normal \<Esc>"]])
expect.equality(#helpers.floats(), 2)
expect.equality(helpers.notified()[#helpers.notified()], "Save with :w, or discard with :q!")
vim.bo[comment_buf].modified = false
vim.cmd([[execute "normal \<Esc>"]])
expect.equality(#helpers.every_float(), 0)
expect.equality(vim.bo.buftype, "quickfix")
end)
it("does not leave any card behind when the list closes", function()
vim.cmd.CommentList()
vim.cmd.cclose()
expect.equality(#helpers.every_float(), 0)
end)
end)
describe("comments in several files", function()
before_each(function()
helpers.buffer("a-first.md", { "a" })
vim.cmd("Comment")
helpers.write("in a-first")
end)
it("are listed by file, then line", function()
vim.cmd("CommentList")
local files = vim.tbl_map(function(item)
return vim.fn.fnamemodify(vim.fn.bufname(item.bufnr), ":t")
end, vim.fn.getqflist())
expect.equality(table.concat(files, ","), "a-first.md,export-fixture.lua,export-fixture.lua")
vim.cmd("cclose")
end)
it("are exported by file, then line", function()
local markdown = scratch.render()
expect.equality(
markdown:find("## a-first.md", 1, true) < markdown:find("## export-fixture.lua", 1, true),
true
)
end)
end)
describe("a snippet containing a code fence", function()
it("is fenced with four backticks", function()
helpers.buffer("fence.md", { "```lua", "x = 1", "```" })
vim.cmd("1,3Comment")
helpers.write("on a fence")
expect.no_equality(scratch.render():find("````md\n```lua", 1, true), nil)
end)
end)