Retain scratch comments across buffer lifecycle changes #2
No files matched your search
@@ -49,21 +49,15 @@ comment on them, or just a word or phrase to comment on only that:
|
||||
:'<,'>Comment
|
||||
```
|
||||
|
||||
a window opens in the middle of the screen, with the lines you're commenting on
|
||||
a window opens over the file, with the lines you're commenting on
|
||||
at the top and your comment below. a new comment opens in insert mode, and the
|
||||
title shows the mode you're in. write as many lines as you like, then `:wq` to
|
||||
save. `:q` or `<Esc>` closes it if you haven't changed anything, and `:q!`
|
||||
throws your changes away. `<C-w>w` moves between the two parts, or scroll them with the
|
||||
mouse.
|
||||
mouse. commenting on an exact range again opens its comment for editing.
|
||||
|
||||
commented lines get a mark in the sign column: `│` for one line, and `╭` `│` `╰`
|
||||
down a range. to read the comment on the cursor line:
|
||||
|
||||
```vim
|
||||
:CommentShow
|
||||
```
|
||||
|
||||
browse every comment, then copy them all:
|
||||
down a range. browse your comments, then copy them all:
|
||||
|
||||
```vim
|
||||
:CommentList
|
||||
@@ -83,12 +77,9 @@ it, pipe it to a command, or edit it:
|
||||
|
||||
| Command | Does |
|
||||
| --- | --- |
|
||||
| `:Comment` | Comment on the current line or command range |
|
||||
| `:CommentShow` | Show the comment at the cursor, with the lines it's on. Edit it there and `:w` to save |
|
||||
| `:CommentNext` | Go to the next comment in the file |
|
||||
| `:CommentPrev` | Go to the previous comment in the file |
|
||||
| `:CommentDelete` | Delete the comment at the cursor, or pick an orphaned one |
|
||||
| `:Comment` | Add or edit a comment in a file; in the built-in comment list, open the cursor row's card |
|
||||
| `:CommentList [filter]` | Put comments in the quickfix list and open it, fuzzy matching `filter` |
|
||||
| `:CommentDelete` | Delete the cursor row in the built-in quickfix list, or the open card elsewhere |
|
||||
| `:CommentExport[!] [format]` | Copy all comments to the clipboard as `markdown` (default) or `json`. `!` opens them in a scratch buffer |
|
||||
| `:CommentToggle [on\|off]` | Show or hide the comment signs |
|
||||
| `:CommentClear` | Delete every comment |
|
||||
@@ -100,23 +91,21 @@ add your own, for example:
|
||||
```lua
|
||||
vim.keymap.set("n", "<leader>ca", "<Cmd>Comment<CR>", { desc = "Comment on line" })
|
||||
vim.keymap.set("x", "<leader>ca", ":Comment<CR>", { desc = "Comment on selection" })
|
||||
vim.keymap.set("n", "<leader>cs", "<Cmd>CommentShow<CR>", { desc = "Show comments" })
|
||||
vim.keymap.set("n", "<leader>cn", "<Cmd>CommentNext<CR>", { desc = "Next comment" })
|
||||
vim.keymap.set("n", "<leader>cp", "<Cmd>CommentPrev<CR>", { desc = "Previous comment" })
|
||||
vim.keymap.set("n", "<leader>cl", "<Cmd>CommentList<CR>", { desc = "List comments" })
|
||||
vim.keymap.set("n", "<leader>cx", "<Cmd>CommentExport<CR>", { desc = "Copy comments" })
|
||||
```
|
||||
|
||||
## browsing
|
||||
|
||||
`:CommentNext` and `:CommentPrev` move between comments in the current file,
|
||||
wrapping at the ends. with a comment open, they show the next or previous
|
||||
comment in that window instead. save or discard your changes first.
|
||||
|
||||
`:CommentList` puts all your comments in the quickfix list, so `]q` and `[q`
|
||||
move between them across files. while the list is open, the comment under the
|
||||
cursor is shown in a card above it. give it an argument to list only the
|
||||
comments that fuzzy match it, in their text, snippet or path:
|
||||
`:CommentList` puts all your comments in the quickfix list. move through them
|
||||
with `j`/`k` without moving the source cursor or opening a card. `<CR>` jumps
|
||||
to the source without opening a card. `:Comment` opens the comment under the
|
||||
quickfix cursor for editing without jumping; its card stays over the file area
|
||||
so the list remains visible. `:w` saves; `:CommentDelete` deletes the comment
|
||||
under the quickfix cursor, or the open card when outside the list. `<Esc>`
|
||||
closes a clean card; unsaved edits need `:w` or `:q!`. an orphan has no line to
|
||||
jump to, but `:Comment` still opens its card. give `:CommentList` an argument to
|
||||
fuzzy match text, snippet or path:
|
||||
|
||||
```vim
|
||||
:CommentList typo
|
||||
@@ -133,21 +122,24 @@ any quickfix viewer works:
|
||||
|
||||
## retention
|
||||
|
||||
comments go away when you close the buffer (`:bd`, `:bw`) or run
|
||||
`:CommentClear`. to keep them, export them to a file.
|
||||
comments stay when you close a buffer (`:bd`, `:bw`). open the same source
|
||||
again and they'll pick up where they left off if the text is still on the same
|
||||
lines. delete one from the built-in quickfix list with `:CommentDelete`, from
|
||||
its open card, or by saving its comment empty. `:CommentClear` deletes them all. to keep them after exiting neovim,
|
||||
export them to a file.
|
||||
|
||||
the signs share the sign column with plugins like gitsigns, and can cover their
|
||||
signs. `:CommentToggle` hides ours when you need to see theirs.
|
||||
|
||||
if you delete all the lines a comment is on, the comment becomes an orphan.
|
||||
orphans are listed last in `:CommentList` and in an "Orphaned" section of the
|
||||
export, and undo restores them. to delete one, run `:CommentDelete` on a line
|
||||
with no comment.
|
||||
if the lines a comment is on are deleted, it becomes an orphan. orphans are
|
||||
listed last in `:CommentList` and in an "Orphaned" section of the export. undo
|
||||
restores them while the buffer is open. delete one from the built-in quickfix
|
||||
list with `:CommentDelete`, or from its open card.
|
||||
|
||||
## colors
|
||||
|
||||
`ScratchCommentSign` colors the signs (linked to `Todo` by default).
|
||||
`ScratchCommentBackdrop` dims the editor behind comments and `:CommentList`
|
||||
`ScratchCommentBackdrop` dims the editor behind an open comment card
|
||||
(black at 60% blend by default).
|
||||
|
||||
set them in your colorscheme's highlight callback to follow light and dark
|
||||
@@ -170,10 +162,7 @@ vim.api.nvim_set_hl(0, "ScratchCommentBackdrop", { bg = "NONE" })
|
||||
local scratch = require("scratch_comments")
|
||||
|
||||
scratch.add(start_line, end_line) -- default: the cursor line
|
||||
scratch.show()
|
||||
scratch.next()
|
||||
scratch.prev()
|
||||
scratch.delete()
|
||||
scratch.delete() -- quickfix cursor row or open card
|
||||
scratch.list()
|
||||
scratch.render(format) -- "markdown" (default) or "json"
|
||||
scratch.export(format, in_buffer) -- copy, or open in a scratch buffer
|
||||
@@ -199,8 +188,8 @@ vim.fn.writefile(vim.split(scratch.render(), "\n"), "review.md")
|
||||
plugin/scratch-comments.lua the commands
|
||||
lua/scratch_comments/
|
||||
init.lua the lua API
|
||||
actions.lua comment, show, delete, toggle, clear
|
||||
navigate.lua next and prev
|
||||
actions.lua add and edit comments, toggle signs, clear
|
||||
list.lua browse, navigate and delete comments
|
||||
comments.lua changes comments and keeps the signs in sync
|
||||
location.lua "line 3", "lines 3–5", "line 3, col 5–12"
|
||||
paths.lua git root and relative paths
|
||||
|
||||
+51
-56
@@ -15,10 +15,10 @@ CONTENTS *scratch-comments-contents*
|
||||
|
||||
Comment on any lines in any file, then copy the comments out as Markdown or
|
||||
JSON to paste into an LLM or give to a person. Commented lines get a sign,
|
||||
and |:CommentShow| shows the comment on the cursor line.
|
||||
and |:CommentList| lets you browse and edit them.
|
||||
|
||||
Comments live in the buffer. They go away when you close the buffer or clear
|
||||
them.
|
||||
Comments live in memory for the Neovim session. Closing a buffer does not
|
||||
delete its comments; |:CommentClear| or exiting Neovim does.
|
||||
|
||||
The commands work as soon as the plugin is installed. There is no setup.
|
||||
Add your own mappings. See |scratch-comments-api| for examples.
|
||||
@@ -34,9 +34,12 @@ Requires Neovim 0.12 or newer.
|
||||
gives ":'<,'>Comment". A characterwise selection
|
||||
(|v|) comments on just the selected text. A linewise
|
||||
one (|V|) comments on whole lines. If that exact text
|
||||
already has a comment, edit it.
|
||||
already has a comment, open it for editing instead of
|
||||
adding a duplicate. In the built-in quickfix list
|
||||
opened by |:CommentList|, open the comment under the
|
||||
cursor for editing without jumping to its source.
|
||||
|
||||
A window opens in the middle of the screen: the lines
|
||||
A window opens over the file: the lines
|
||||
you are commenting on at the top, your comment below.
|
||||
A new comment opens in insert mode, and the title
|
||||
shows the mode you are in. Write as many lines as you
|
||||
@@ -45,47 +48,32 @@ Requires Neovim 0.12 or newer.
|
||||
|CTRL-W_w| moves between the two parts, or scroll them
|
||||
with the mouse.
|
||||
|
||||
*:CommentShow*
|
||||
:CommentShow Open the comment at the cursor, with the lines it is
|
||||
on, in the same window |:Comment| uses. If more than
|
||||
one comment covers the line, asks which one. It opens
|
||||
in normal mode. Edit the comment and |:w| to save, or
|
||||
|:q| to close it.
|
||||
|
||||
*:CommentNext*
|
||||
:CommentNext Go to the next comment in the current file, wrapping
|
||||
at the end. Comments that start on the same line are
|
||||
one stop.
|
||||
|
||||
With a comment open, show the next comment in that
|
||||
window instead. Every comment is a stop, even on the
|
||||
same line, and the cursor in the file follows. Save
|
||||
or discard your changes first.
|
||||
|
||||
*:CommentPrev*
|
||||
:CommentPrev Go to the previous comment in the current file,
|
||||
wrapping at the start. With a comment open, show the
|
||||
previous comment in that window instead.
|
||||
|
||||
*:CommentDelete*
|
||||
:CommentDelete Delete the comment at the cursor. If more than one
|
||||
comment covers the line, asks which one. On a line
|
||||
with no comment, lists the buffer's orphaned comments
|
||||
to choose from.
|
||||
|
||||
*:CommentList*
|
||||
:CommentList [filter] Put every comment in the |quickfix| list and open it.
|
||||
With [filter], only the comments that fuzzy match it
|
||||
in their text, snippet or path (see |matchfuzzy()|).
|
||||
|]q| and |[q| then move between comments across files.
|
||||
For a comment on selected text, they land on the text.
|
||||
|
||||
While the list is open, the comment the cursor is on
|
||||
is shown in a card above it, with the lines it is on.
|
||||
The card goes away with the list.
|
||||
Any quickfix front-end (trouble.nvim, nvim-bqf,
|
||||
Moving through the built-in list does not move the
|
||||
source cursor or open a card. <CR> jumps to the
|
||||
comment's source location without opening a card.
|
||||
For an orphan there is no location to jump to; the
|
||||
list stays open. |:Comment| opens the cursor row's
|
||||
card for editing without moving the source cursor.
|
||||
The card opens in normal mode. |:w| saves; saving it
|
||||
empty deletes it. <Esc> closes a clean card from
|
||||
the list or the card; unsaved changes require |:w| or
|
||||
|:q!|. With no card open, <Esc> does nothing.
|
||||
Other quickfix front-ends (trouble.nvim, nvim-bqf,
|
||||
Snacks.picker.qflist, :Telescope quickfix) can show
|
||||
it.
|
||||
the entries; the cursor-row commands and <CR>/<Esc>
|
||||
belong to the built-in list only.
|
||||
|
||||
*:CommentDelete*
|
||||
:CommentDelete In the built-in quickfix list opened by |:CommentList|,
|
||||
delete the comment under the cursor without opening
|
||||
its card. Elsewhere, delete the comment in the open
|
||||
card. A different quickfix list is never a target.
|
||||
Save or discard unsaved edits in the targeted card
|
||||
first.
|
||||
|
||||
*:CommentExport*
|
||||
:CommentExport[!] [format]
|
||||
@@ -109,21 +97,31 @@ Requires Neovim 0.12 or newer.
|
||||
A comment belongs to a file and a range of lines, or to the exact text you
|
||||
selected within them.
|
||||
|
||||
- Commenting on the same lines again edits that comment.
|
||||
- Commenting on the same lines again opens that comment for editing instead
|
||||
of adding a duplicate.
|
||||
- Commenting on different lines adds a new comment, even if the lines overlap
|
||||
another comment. A comment on lines 10–30 and one on line 14 are separate.
|
||||
|
||||
==============================================================================
|
||||
4. RETENTION *scratch-comments-retention*
|
||||
|
||||
Comments go away when the buffer is deleted (|:bdelete|, |:bwipeout|) or when
|
||||
you run |:CommentClear|. Exporting leaves them in place, so you can export as
|
||||
often as you like. To keep comments, save an export to a file.
|
||||
Closing, unloading or wiping a buffer does not delete its comments. They
|
||||
remain in memory with their last location and snippet until you delete them,
|
||||
run |:CommentClear|, or exit Neovim. Exporting leaves them in place. Save an
|
||||
export to a file if you need to keep comments after exiting Neovim.
|
||||
|
||||
If you delete all the lines a comment is on, the comment becomes an orphan.
|
||||
Orphans are listed last in |:CommentList| and in an "Orphaned" section of the
|
||||
export, and |undo| restores them. To delete one, run |:CommentDelete| on a
|
||||
line with no comment.
|
||||
When the same source is reopened, signs return if the saved text still occupies
|
||||
the same range. Comments are not automatically moved to other lines. For
|
||||
asynchronously loaded sources, a comment may remain active without a sign
|
||||
until its text arrives; an incomplete load is not treated as an orphan.
|
||||
|
||||
If you delete all the lines a comment is on, the comment becomes inactive (an
|
||||
orphan). A completed ordinary file reload with mismatched text also makes a
|
||||
parked comment inactive. Inactive comments are listed last in |:CommentList|
|
||||
and in an "Orphaned" section of the export; |undo| restores an extmark whose
|
||||
buffer is still present. Delete an inactive comment with |:CommentDelete|
|
||||
from the built-in quickfix list or its open card, or open it with |:Comment|
|
||||
and save it empty.
|
||||
|
||||
In tmux you may need to set the clipboard to OSC 52 yourself: >lua
|
||||
vim.g.clipboard = "osc52"
|
||||
@@ -144,8 +142,8 @@ The signs use ScratchCommentSign, which links to |hl-Todo|. To change it: >lua
|
||||
})
|
||||
<
|
||||
*ScratchCommentBackdrop*
|
||||
The editor dims behind the comment window, and behind the card beside
|
||||
|:CommentList|, with ScratchCommentBackdrop over it at 'winblend' 60. It
|
||||
The editor dims behind the comment window, with ScratchCommentBackdrop over
|
||||
it at 'winblend' 60. It
|
||||
needs 'termguicolors'. For a different dim, or none: >lua
|
||||
vim.api.nvim_set_hl(0, "ScratchCommentBackdrop", { bg = "#000033" })
|
||||
vim.api.nvim_set_hl(0, "ScratchCommentBackdrop", { bg = "NONE" })
|
||||
@@ -158,10 +156,7 @@ needs 'termguicolors'. For a different dim, or none: >lua
|
||||
local scratch = require("scratch_comments")
|
||||
|
||||
scratch.add(start_line, end_line) -- default: the cursor line
|
||||
scratch.show()
|
||||
scratch.next()
|
||||
scratch.prev()
|
||||
scratch.delete()
|
||||
scratch.delete() -- comment in the open card
|
||||
scratch.list()
|
||||
scratch.render(format) -- "markdown" (default) or "json"
|
||||
scratch.export(format, in_buffer) -- copy, or open in a scratch buffer
|
||||
@@ -171,7 +166,7 @@ needs 'termguicolors'. For a different dim, or none: >lua
|
||||
Example mappings: >lua
|
||||
vim.keymap.set("n", "<leader>ca", "<Cmd>Comment<CR>")
|
||||
vim.keymap.set("x", "<leader>ca", ":Comment<CR>")
|
||||
vim.keymap.set("n", "<leader>cs", "<Cmd>CommentShow<CR>")
|
||||
vim.keymap.set("n", "<leader>cl", "<Cmd>CommentList<CR>")
|
||||
vim.keymap.set("n", "<leader>cx", "<Cmd>CommentExport<CR>")
|
||||
<
|
||||
vim:tw=78:ts=8:noet:ft=help:norl:
|
||||
@@ -1,6 +1,7 @@
|
||||
local comments = require("scratch_comments.comments")
|
||||
local location = require("scratch_comments.location")
|
||||
local paths = require("scratch_comments.paths")
|
||||
local store = require("scratch_comments.model.store")
|
||||
local views = require("scratch_comments.model.views")
|
||||
local notify = require("scratch_comments.ui.notify")
|
||||
local card = require("scratch_comments.ui.card")
|
||||
@@ -9,12 +10,6 @@ local window = require("scratch_comments.ui.window")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param text string
|
||||
---@return string
|
||||
local function summary(text)
|
||||
return vim.split(text, "\n")[1]
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param start_line integer
|
||||
---@param end_line integer
|
||||
@@ -45,29 +40,49 @@ local function selected_columns(start_line, end_line)
|
||||
return from[3] - 1, end_col
|
||||
end
|
||||
|
||||
---@return ScratchCommentView[]
|
||||
local function cursor_comments()
|
||||
local line = vim.api.nvim_win_get_cursor(0)[1]
|
||||
return vim.tbl_filter(function(view)
|
||||
return view.start_line <= line and line <= view.end_line
|
||||
end, views.in_buffer(vim.api.nvim_get_current_buf()))
|
||||
---@param id string
|
||||
---@param text string
|
||||
---@return boolean|string? "deleted" when saving removes the comment
|
||||
local function save_existing(id, text)
|
||||
local comment = vim.iter(store.all()):find(function(candidate)
|
||||
return candidate.id == id
|
||||
end)
|
||||
if not comment then
|
||||
notify.warn("This comment is gone; nothing was saved")
|
||||
return false
|
||||
end
|
||||
if text == "" then
|
||||
comments.delete(comment)
|
||||
notify.info("Deleted comment")
|
||||
return "deleted"
|
||||
end
|
||||
comments.edit(id, text)
|
||||
notify.info("Updated comment")
|
||||
end
|
||||
|
||||
---@param view ScratchCommentView
|
||||
---@param view ScratchComment|ScratchCommentView
|
||||
---@param source_win integer
|
||||
function M.show(view, source_win)
|
||||
window.open(
|
||||
vim.tbl_extend("error", card.of(view), {
|
||||
on_save = function(text)
|
||||
if not comments.edit(view.id, text) then
|
||||
notify.warn("This comment is gone; nothing was saved")
|
||||
return false
|
||||
end
|
||||
notify.info("Updated comment")
|
||||
end,
|
||||
}),
|
||||
{ id = view.id, bufnr = view.bufnr, line = view.start_line, source_win = source_win }
|
||||
)
|
||||
---@param on_close? fun()
|
||||
---@param on_saved? fun()
|
||||
function M.show(view, source_win, on_close, on_saved)
|
||||
local frame
|
||||
if view.start_line then
|
||||
frame = card.of(view --[[@as ScratchCommentView]])
|
||||
else
|
||||
frame = card.of_orphan(view)
|
||||
end
|
||||
frame.on_save = function(text)
|
||||
return save_existing(view.id, text)
|
||||
end
|
||||
window.open(frame, {
|
||||
id = view.id,
|
||||
bufnr = view.bufnr,
|
||||
source_name = view.source_name,
|
||||
line = view.start_line or view.last_location.start_line,
|
||||
source_win = source_win,
|
||||
on_close = on_close,
|
||||
on_saved = on_saved,
|
||||
})
|
||||
end
|
||||
|
||||
-- Opens an editor for a comment that does not exist yet; it is created when
|
||||
@@ -76,12 +91,21 @@ end
|
||||
---@param file_path string
|
||||
---@param where ScratchLocation
|
||||
local function edit_new(bufnr, file_path, where)
|
||||
local relative_path = paths.relative_path(paths.git_root(file_path), file_path)
|
||||
local relative_path = paths.is_uri(file_path) and file_path
|
||||
or paths.relative_path(paths.git_root(file_path), file_path)
|
||||
---@type ScratchComment?
|
||||
local added
|
||||
---@type ScratchCommentWindow
|
||||
local record =
|
||||
{ bufnr = bufnr, line = where.start_line, source_win = vim.api.nvim_get_current_win() }
|
||||
local record = {
|
||||
bufnr = bufnr,
|
||||
line = where.start_line,
|
||||
source_win = vim.api.nvim_get_current_win(),
|
||||
on_saved = function()
|
||||
if added then
|
||||
require("scratch_comments.list").refresh_comment(added.id)
|
||||
end
|
||||
end,
|
||||
}
|
||||
window.open({
|
||||
title = location.title(file_path, where),
|
||||
context = views.text(bufnr, where.start_line, where.end_line, where.start_col, where.end_col),
|
||||
@@ -89,19 +113,16 @@ local function edit_new(bufnr, file_path, where)
|
||||
comment = "",
|
||||
on_save = function(text)
|
||||
if added then
|
||||
if not comments.edit(added.id, text) then
|
||||
notify.warn("This comment is gone; nothing was saved")
|
||||
return false
|
||||
end
|
||||
return
|
||||
return save_existing(added.id, text)
|
||||
end
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
if text == "" or not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
added = comments.add({
|
||||
bufnr = bufnr,
|
||||
comment = text,
|
||||
file_path = file_path,
|
||||
source_name = file_path,
|
||||
relative_path = relative_path,
|
||||
}, where)
|
||||
record.id = added.id
|
||||
@@ -115,6 +136,12 @@ end
|
||||
---@param use_selection? boolean
|
||||
function M.comment(start_line, end_line, use_selection)
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
if vim.bo[bufnr].buftype == "quickfix" then
|
||||
if not require("scratch_comments.list").edit_selected() then
|
||||
notify.warn("Not a Scratch Comments list")
|
||||
end
|
||||
return
|
||||
end
|
||||
local name = vim.api.nvim_buf_get_name(bufnr)
|
||||
if name == "" then
|
||||
notify.warn("Save the buffer before commenting on it")
|
||||
@@ -133,77 +160,13 @@ function M.comment(start_line, end_line, use_selection)
|
||||
local existing =
|
||||
find_anchor(bufnr, where.start_line, where.end_line, where.start_col, where.end_col)
|
||||
if existing then
|
||||
M.show(existing, vim.api.nvim_get_current_win())
|
||||
M.show(existing, vim.api.nvim_get_current_win(), nil, function()
|
||||
require("scratch_comments.list").refresh_comment(existing.id)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
edit_new(bufnr, vim.fn.fnamemodify(name, ":p"), where)
|
||||
end
|
||||
|
||||
---@param view ScratchCommentView
|
||||
---@return string
|
||||
local function describe(view)
|
||||
return location.describe(view) .. ": " .. summary(view.comment)
|
||||
end
|
||||
|
||||
---@param candidates ScratchCommentView[]
|
||||
---@param callback fun(view: ScratchCommentView)
|
||||
local function choose(candidates, callback)
|
||||
if #candidates == 1 then
|
||||
callback(candidates[1])
|
||||
return
|
||||
end
|
||||
vim.ui.select(candidates, { prompt = "Comment: ", format_item = describe }, function(view)
|
||||
if view then
|
||||
callback(view)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param comment ScratchComment
|
||||
local function delete(comment)
|
||||
comments.delete(comment)
|
||||
notify.info("Deleted comment")
|
||||
end
|
||||
|
||||
function M.show_current()
|
||||
local at_cursor = cursor_comments()
|
||||
if #at_cursor == 0 then
|
||||
notify.info("No comment at cursor")
|
||||
return
|
||||
end
|
||||
local source_win = vim.api.nvim_get_current_win()
|
||||
choose(at_cursor, function(view)
|
||||
M.show(view, source_win)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.delete_current()
|
||||
local at_cursor = cursor_comments()
|
||||
if #at_cursor > 0 then
|
||||
choose(at_cursor, delete)
|
||||
return
|
||||
end
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local orphans = vim.tbl_filter(function(comment)
|
||||
return comment.bufnr == bufnr
|
||||
end, views.orphans())
|
||||
if #orphans == 0 then
|
||||
notify.info("No comment at cursor")
|
||||
return
|
||||
end
|
||||
|
||||
vim.ui.select(orphans, {
|
||||
prompt = "Delete orphaned comment: ",
|
||||
format_item = function(comment)
|
||||
return summary(comment.comment)
|
||||
end,
|
||||
}, function(comment)
|
||||
if comment then
|
||||
delete(comment)
|
||||
end
|
||||
end)
|
||||
edit_new(bufnr, paths.is_uri(name) and name or vim.fn.fnamemodify(name, ":p"), where)
|
||||
end
|
||||
|
||||
---@param on? boolean
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
local anchors = require("scratch_comments.model.anchors")
|
||||
local paths = require("scratch_comments.paths")
|
||||
local signs = require("scratch_comments.ui.signs")
|
||||
local store = require("scratch_comments.model.store")
|
||||
local views = require("scratch_comments.model.views")
|
||||
|
||||
local M = {}
|
||||
|
||||
local group = vim.api.nvim_create_augroup("scratch_comments", { clear = true })
|
||||
local watched = {}
|
||||
|
||||
---@param bufnr integer
|
||||
function M.redraw(bufnr)
|
||||
signs.draw(bufnr, store.in_buffer(bufnr))
|
||||
if vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr) then
|
||||
signs.draw(bufnr, store.in_buffer(bufnr))
|
||||
end
|
||||
end
|
||||
|
||||
---@return integer[]
|
||||
local function commented_buffers()
|
||||
local seen = {}
|
||||
for _, comment in ipairs(store.all()) do
|
||||
seen[comment.bufnr] = true
|
||||
if comment.bufnr then
|
||||
seen[comment.bufnr] = true
|
||||
end
|
||||
end
|
||||
return vim.tbl_keys(seen)
|
||||
end
|
||||
|
||||
---@param fields { bufnr: integer, comment: string, file_path: string, relative_path: string }
|
||||
---@param fields { bufnr: integer, comment: string, file_path: string, source_name?: string, relative_path: string }
|
||||
---@param location ScratchLocation
|
||||
---@return ScratchComment
|
||||
function M.add(fields, location)
|
||||
@@ -29,6 +36,18 @@ function M.add(fields, location)
|
||||
comment = fields.comment,
|
||||
file_path = fields.file_path,
|
||||
relative_path = fields.relative_path,
|
||||
source_name = fields.source_name,
|
||||
last_location = vim.deepcopy(location),
|
||||
snippet = table.concat(
|
||||
views.text(
|
||||
fields.bufnr,
|
||||
location.start_line,
|
||||
location.end_line,
|
||||
location.start_col,
|
||||
location.end_col
|
||||
),
|
||||
"\n"
|
||||
),
|
||||
charwise = location.start_col ~= nil,
|
||||
})
|
||||
anchors.anchor(
|
||||
@@ -38,7 +57,7 @@ function M.add(fields, location)
|
||||
location.start_col,
|
||||
location.end_col
|
||||
)
|
||||
M.redraw(comment.bufnr)
|
||||
M.redraw(fields.bufnr)
|
||||
return comment
|
||||
end
|
||||
|
||||
@@ -54,7 +73,9 @@ function M.delete(comment)
|
||||
anchors.clear_all(store.remove(function(candidate)
|
||||
return candidate.id == comment.id
|
||||
end))
|
||||
M.redraw(comment.bufnr)
|
||||
if comment.bufnr then
|
||||
M.redraw(comment.bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
function M.clear()
|
||||
@@ -65,13 +86,84 @@ function M.clear()
|
||||
store.clear()
|
||||
end
|
||||
|
||||
-- Snapshot while extmarks and text still exist. BufUnload, BufDelete and
|
||||
-- BufWipeout can all fire for the same buffer; only the first does work.
|
||||
---@param bufnr integer
|
||||
function M.forget_buffer(bufnr)
|
||||
anchors.clear_buffer(bufnr)
|
||||
signs.clear_buffer(bufnr)
|
||||
store.remove(function(comment)
|
||||
return comment.bufnr == bufnr
|
||||
end)
|
||||
function M.park_buffer(bufnr)
|
||||
for _, comment in ipairs(store.in_buffer(bufnr)) do
|
||||
local start_line, end_line, start_col, end_col = anchors.range(comment)
|
||||
if start_line then
|
||||
end_line = assert(end_line)
|
||||
comment.last_location = {
|
||||
start_line = start_line,
|
||||
end_line = end_line,
|
||||
start_col = start_col,
|
||||
end_col = end_col,
|
||||
}
|
||||
comment.snippet =
|
||||
table.concat(views.text(bufnr, start_line, end_line, start_col, end_col), "\n")
|
||||
comment.state = "parked"
|
||||
else
|
||||
comment.state = "inactive"
|
||||
end
|
||||
anchors.clear_all({ comment })
|
||||
comment.bufnr, comment.extmark_id = nil, nil
|
||||
end
|
||||
end
|
||||
|
||||
-- An active source may reopen under a different bufnr. Its old line number is
|
||||
-- only trusted when the saved text still occupies that exact range.
|
||||
---@param bufnr integer
|
||||
---@param definitive? boolean true after a completed ordinary file read
|
||||
function M.attach_buffer(bufnr, definitive)
|
||||
if not vim.api.nvim_buf_is_loaded(bufnr) then
|
||||
return
|
||||
end
|
||||
local name = vim.api.nvim_buf_get_name(bufnr)
|
||||
local is_uri = paths.is_uri(name)
|
||||
-- BufReadCmd providers can return from the command before asynchronously
|
||||
-- supplying any text. The initial empty line is not a completed read.
|
||||
if is_uri then
|
||||
local first = vim.api.nvim_buf_get_lines(bufnr, 0, 1, false)[1]
|
||||
if first == "" and vim.api.nvim_buf_line_count(bufnr) == 1 then
|
||||
return
|
||||
end
|
||||
end
|
||||
local changed = false
|
||||
for _, comment in ipairs(store.all()) do
|
||||
-- An async provider can replace a partial, already-matching range. That
|
||||
-- does not prove the source lines were deleted; retry after the update.
|
||||
if is_uri and comment.bufnr == bufnr and anchors.is_orphaned(comment) then
|
||||
anchors.clear_all({ comment })
|
||||
comment.bufnr, comment.extmark_id = nil, nil
|
||||
comment.state = "parked"
|
||||
changed = true
|
||||
end
|
||||
if comment.source_name == name and comment.state == "parked" then
|
||||
local where = comment.last_location
|
||||
local count = vim.api.nvim_buf_line_count(bufnr)
|
||||
if where.end_line <= count then
|
||||
local ok, text =
|
||||
pcall(views.text, bufnr, where.start_line, where.end_line, where.start_col, where.end_col)
|
||||
if ok and table.concat(text, "\n") == comment.snippet then
|
||||
comment.bufnr = bufnr
|
||||
anchors.anchor(comment, where.start_line, where.end_line, where.start_col, where.end_col)
|
||||
comment.state = nil
|
||||
changed = true
|
||||
elseif definitive then
|
||||
comment.state = "inactive"
|
||||
changed = true
|
||||
end
|
||||
elseif definitive then
|
||||
comment.state = "inactive"
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if changed then
|
||||
M.redraw(bufnr)
|
||||
require("scratch_comments.list").source_updated(bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
---@param on boolean
|
||||
@@ -89,10 +181,71 @@ vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("BufDelete", {
|
||||
vim.api.nvim_create_autocmd({ "BufUnload", "BufDelete", "BufWipeout" }, {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
M.forget_buffer(args.buf)
|
||||
M.park_buffer(args.buf)
|
||||
end,
|
||||
})
|
||||
|
||||
-- A read has completed for normal files. BufReadCmd sources may instead fill
|
||||
-- the buffer later; watch their text changes without depending on the provider.
|
||||
vim.api.nvim_create_autocmd("BufReadPost", {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
vim.schedule(function()
|
||||
if vim.api.nvim_buf_is_valid(args.buf) then
|
||||
M.attach_buffer(args.buf, true)
|
||||
end
|
||||
end)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("BufEnter", {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
local bufnr = args.buf
|
||||
if vim.api.nvim_buf_is_loaded(bufnr) then
|
||||
local name = vim.api.nvim_buf_get_name(bufnr)
|
||||
local has_parked = vim.iter(store.all()):any(function(comment)
|
||||
return comment.source_name == name and comment.state == "parked"
|
||||
end)
|
||||
if not has_parked then
|
||||
return
|
||||
end
|
||||
if not watched[bufnr] then
|
||||
watched[bufnr] = vim.api.nvim_buf_attach(bufnr, false, {
|
||||
on_lines = function()
|
||||
vim.schedule(function()
|
||||
if vim.api.nvim_buf_is_valid(bufnr) then
|
||||
M.attach_buffer(bufnr)
|
||||
end
|
||||
end)
|
||||
if
|
||||
not paths.is_uri(name)
|
||||
and not vim.iter(store.all()):any(function(comment)
|
||||
return comment.source_name == name and comment.state == "parked"
|
||||
end)
|
||||
then
|
||||
return true
|
||||
end
|
||||
end,
|
||||
on_detach = function()
|
||||
watched[bufnr] = nil
|
||||
end,
|
||||
})
|
||||
end
|
||||
-- A previously loaded file can be reentered without a read event. For
|
||||
-- URI buffers an initial empty pane may still be waiting for BufReadCmd.
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, 1, false)
|
||||
if not paths.is_uri(name) or lines[1] ~= "" then
|
||||
vim.schedule(function()
|
||||
if vim.api.nvim_buf_is_valid(bufnr) then
|
||||
M.attach_buffer(bufnr)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
local actions = require("scratch_comments.actions")
|
||||
local export = require("scratch_comments.export")
|
||||
local list = require("scratch_comments.list")
|
||||
local navigate = require("scratch_comments.navigate")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -12,22 +11,13 @@ function M.add(start_line, end_line)
|
||||
actions.comment(start_line, end_line or start_line)
|
||||
end
|
||||
|
||||
M.show = actions.show_current
|
||||
M.delete = actions.delete_current
|
||||
M.toggle = actions.toggle
|
||||
M.clear = actions.clear
|
||||
M.render = export.render
|
||||
M.export = export.export
|
||||
|
||||
function M.next()
|
||||
navigate.jump(1)
|
||||
end
|
||||
|
||||
function M.prev()
|
||||
navigate.jump(-1)
|
||||
end
|
||||
|
||||
M.list = list.open
|
||||
M.delete = list.delete_selected
|
||||
|
||||
-- Kept so configs that call it keep working; the commands need no setup.
|
||||
function M.setup() end
|
||||
|
||||
+339
-21
@@ -1,5 +1,6 @@
|
||||
local card = require("scratch_comments.ui.card")
|
||||
local preview = require("scratch_comments.ui.preview")
|
||||
local actions = require("scratch_comments.actions")
|
||||
local comments = require("scratch_comments.comments")
|
||||
local store = require("scratch_comments.model.store")
|
||||
local notify = require("scratch_comments.ui.notify")
|
||||
local quickfix = require("scratch_comments.ui.quickfix")
|
||||
local window = require("scratch_comments.ui.window")
|
||||
@@ -7,6 +8,13 @@ local views = require("scratch_comments.model.views")
|
||||
|
||||
local M = {}
|
||||
|
||||
local list_id
|
||||
local list_bufnr
|
||||
local active_filter
|
||||
local pending_jump = {}
|
||||
local listening = {}
|
||||
local refresh
|
||||
|
||||
---@param comment ScratchComment|ScratchCommentView
|
||||
---@return string
|
||||
local function haystack(comment)
|
||||
@@ -15,12 +23,12 @@ end
|
||||
|
||||
-- Keeps the comments that fuzzy match, in the order they were listed.
|
||||
---@generic T: ScratchComment
|
||||
---@param comments T[]
|
||||
---@param entries T[]
|
||||
---@param filter string
|
||||
---@return T[]
|
||||
local function matching(comments, filter)
|
||||
local function matching(entries, filter)
|
||||
local candidates = {}
|
||||
for index, comment in ipairs(comments) do
|
||||
for index, comment in ipairs(entries) do
|
||||
table.insert(candidates, { index = index, text = haystack(comment) })
|
||||
end
|
||||
local kept = {}
|
||||
@@ -29,7 +37,7 @@ local function matching(comments, filter)
|
||||
end
|
||||
|
||||
local matched = {}
|
||||
for index, comment in ipairs(comments) do
|
||||
for index, comment in ipairs(entries) do
|
||||
if kept[index] then
|
||||
table.insert(matched, comment)
|
||||
end
|
||||
@@ -37,29 +45,339 @@ local function matching(comments, filter)
|
||||
return matched
|
||||
end
|
||||
|
||||
-- Every comment in the quickfix list, with the one under the cursor shown
|
||||
-- in a card above it.
|
||||
---@param filter? string
|
||||
---@return ScratchCommentView[], ScratchComment[]
|
||||
local function filtered(filter)
|
||||
local anchored, orphans = views.anchored(), views.orphans()
|
||||
if filter and filter ~= "" then
|
||||
anchored, orphans = matching(anchored, filter), matching(orphans, filter)
|
||||
end
|
||||
return anchored, orphans
|
||||
end
|
||||
|
||||
---@param id string
|
||||
---@return ScratchComment?
|
||||
local function by_id(id)
|
||||
return vim.iter(store.all()):find(function(comment)
|
||||
return comment.id == id
|
||||
end)
|
||||
end
|
||||
|
||||
---@return integer?, string?
|
||||
local function selected()
|
||||
if vim.api.nvim_get_current_buf() ~= list_bufnr then
|
||||
return nil
|
||||
end
|
||||
local qf = vim.fn.getqflist({ id = 0, items = 1 })
|
||||
if qf.id ~= list_id then
|
||||
return nil
|
||||
end
|
||||
local index = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local data = qf.items[index] and qf.items[index].user_data
|
||||
return index, data and data.scratch_comments_id
|
||||
end
|
||||
|
||||
local function jump()
|
||||
-- Only the most recent manager jump may move the cursor after a delayed read.
|
||||
for bufnr in pairs(pending_jump) do
|
||||
pending_jump[bufnr] = nil
|
||||
end
|
||||
local index, id = selected()
|
||||
if not index then
|
||||
-- Another plugin replaced our quickfix list; leave its normal navigation usable.
|
||||
vim.cmd.cc(vim.api.nvim_win_get_cursor(0)[1])
|
||||
return
|
||||
end
|
||||
if not id then
|
||||
notify.warn("This comment is gone")
|
||||
return
|
||||
end
|
||||
local comment = by_id(id)
|
||||
if not comment then
|
||||
notify.warn("This comment is gone")
|
||||
return
|
||||
end
|
||||
local entry = vim.iter(views.anchored()):find(function(view)
|
||||
return view.id == id
|
||||
end)
|
||||
if not entry then
|
||||
notify.warn("This comment has no source location; use :Comment to edit it")
|
||||
return
|
||||
end
|
||||
if not window.close() then
|
||||
notify.warn("Save or discard the comment first")
|
||||
return
|
||||
end
|
||||
local qf_win = vim.api.nvim_get_current_win()
|
||||
local ok = pcall(function()
|
||||
vim.cmd(index .. "cc")
|
||||
end)
|
||||
if not ok or vim.api.nvim_buf_get_name(0) ~= comment.source_name then
|
||||
notify.warn("Could not open the comment source")
|
||||
return
|
||||
end
|
||||
local bufnr, win = vim.api.nvim_get_current_buf(), vim.api.nvim_get_current_win()
|
||||
local expected_id = list_id
|
||||
local function return_to_manager()
|
||||
if vim.api.nvim_win_is_valid(qf_win) and vim.fn.getqflist({ id = 0 }).id == expected_id then
|
||||
vim.api.nvim_set_current_win(qf_win)
|
||||
refresh(id, index, expected_id)
|
||||
end
|
||||
end
|
||||
-- Loading a parked source can reveal that its saved location is gone.
|
||||
if vim.iter(views.orphans()):find(function(orphan)
|
||||
return orphan.id == id
|
||||
end) then
|
||||
return_to_manager()
|
||||
notify.warn("This comment no longer has a location; use :Comment to edit it")
|
||||
return
|
||||
end
|
||||
local function position()
|
||||
if not vim.api.nvim_win_is_valid(win) or vim.api.nvim_win_get_buf(win) ~= bufnr then
|
||||
return true
|
||||
end
|
||||
if vim.api.nvim_get_current_win() ~= win then
|
||||
return true
|
||||
end
|
||||
local current = by_id(id)
|
||||
if not current then
|
||||
return true
|
||||
end
|
||||
local view = vim.iter(views.in_buffer(bufnr)):find(function(v)
|
||||
return v.id == id
|
||||
end)
|
||||
if view then
|
||||
vim.api.nvim_win_set_cursor(win, { view.start_line, view.start_col or 0 })
|
||||
return true
|
||||
end
|
||||
if current.state == "inactive" then
|
||||
return_to_manager()
|
||||
notify.warn("This comment no longer has a location; use :Comment to edit it")
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
if position() then
|
||||
return
|
||||
end
|
||||
-- An async BufReadCmd may fill the buffer after :cc has already moved the
|
||||
-- cursor. Keep one listener per buffer even if the user jumps repeatedly.
|
||||
pending_jump[bufnr] = position
|
||||
-- A delayed read must not reposition a source the user has left and
|
||||
-- returned to in the meantime.
|
||||
vim.api.nvim_create_autocmd("WinLeave", {
|
||||
once = true,
|
||||
callback = function()
|
||||
if pending_jump[bufnr] == position then
|
||||
pending_jump[bufnr] = nil
|
||||
end
|
||||
end,
|
||||
})
|
||||
-- BufReadPost reattachment runs on the next scheduled turn. Recheck after
|
||||
-- it, even if the file was fully populated before our on_lines listener.
|
||||
vim.schedule(function()
|
||||
if pending_jump[bufnr] == position and position() then
|
||||
pending_jump[bufnr] = nil
|
||||
end
|
||||
end)
|
||||
if listening[bufnr] then
|
||||
return
|
||||
end
|
||||
listening[bufnr] = vim.api.nvim_buf_attach(bufnr, false, {
|
||||
on_lines = function()
|
||||
local current = pending_jump[bufnr]
|
||||
if not current then
|
||||
listening[bufnr] = nil
|
||||
return true
|
||||
end
|
||||
vim.schedule(function()
|
||||
if pending_jump[bufnr] == current and current() then
|
||||
pending_jump[bufnr] = nil
|
||||
end
|
||||
end)
|
||||
end,
|
||||
on_detach = function()
|
||||
pending_jump[bufnr] = nil
|
||||
listening[bufnr] = nil
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
-- Update the manager behind an open editor, without taking focus or opening
|
||||
-- a list that the user already closed. Replace items in the same quickfix ID.
|
||||
---@param id string
|
||||
---@param index integer
|
||||
---@param expected_id integer
|
||||
refresh = function(id, index, expected_id)
|
||||
local qf = vim.fn.getqflist({ id = 0, winid = 0 })
|
||||
if qf.id ~= expected_id or qf.winid == 0 then
|
||||
return
|
||||
end
|
||||
local anchored, orphans = filtered(active_filter)
|
||||
local items = quickfix.items(anchored, orphans)
|
||||
vim.fn.setqflist({}, "r", { title = "Comments", items = items })
|
||||
if #items == 0 then
|
||||
vim.api.nvim_win_call(qf.winid, function()
|
||||
vim.cmd.cclose()
|
||||
end)
|
||||
list_id, list_bufnr = nil, nil
|
||||
return
|
||||
end
|
||||
local row = math.min(index, #items)
|
||||
for i, item in ipairs(items) do
|
||||
if item.user_data.scratch_comments_id == id then
|
||||
row = i
|
||||
break
|
||||
end
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(qf.winid, { row, 0 })
|
||||
end
|
||||
|
||||
-- A card opened from a source window may also change a visible manager list.
|
||||
---@param id? string
|
||||
function M.refresh_comment(id)
|
||||
local qf = vim.fn.getqflist({ id = 0, winid = 0 })
|
||||
if list_id and qf.id == list_id and qf.winid ~= 0 then
|
||||
local row = vim.api.nvim_win_get_cursor(qf.winid)[1]
|
||||
local item = vim.fn.getqflist({ items = 1 }).items[row]
|
||||
local selected_id = item and item.user_data and item.user_data.scratch_comments_id
|
||||
refresh(selected_id or id or "", row, list_id)
|
||||
end
|
||||
end
|
||||
|
||||
-- Recheck a pending navigation after reattachment, which can happen after
|
||||
-- the last on_lines notification from an asynchronous source.
|
||||
---@param bufnr integer
|
||||
function M.source_updated(bufnr)
|
||||
M.refresh_comment()
|
||||
local position = pending_jump[bufnr]
|
||||
if position then
|
||||
vim.schedule(function()
|
||||
if pending_jump[bufnr] == position and position() then
|
||||
pending_jump[bufnr] = nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Open the card for the cursor row without jumping to its source.
|
||||
---@return boolean handled true if this is our quickfix list
|
||||
function M.edit_selected()
|
||||
local index, id = selected()
|
||||
if not index then
|
||||
return false
|
||||
end
|
||||
if not id or not by_id(id) then
|
||||
notify.warn("This comment is gone")
|
||||
return true
|
||||
end
|
||||
local entry = vim.iter(views.anchored()):find(function(view)
|
||||
return view.id == id
|
||||
end) or vim.iter(views.orphans()):find(function(view)
|
||||
return view.id == id
|
||||
end)
|
||||
if not entry then
|
||||
notify.warn("This comment is gone")
|
||||
return true
|
||||
end
|
||||
if not window.close() then
|
||||
notify.warn("Save or discard the comment first")
|
||||
return true
|
||||
end
|
||||
local qf_win = vim.api.nvim_get_current_win()
|
||||
local expected_id = list_id
|
||||
actions.show(entry, qf_win, function()
|
||||
vim.schedule(function()
|
||||
if vim.fn.getqflist({ id = 0 }).id == expected_id and vim.api.nvim_win_is_valid(qf_win) then
|
||||
vim.api.nvim_set_current_win(qf_win)
|
||||
end
|
||||
end)
|
||||
end, function()
|
||||
refresh(id, index, expected_id)
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
-- From our quickfix list, use the cursor row (not the last-jumped quickfix
|
||||
-- index). Elsewhere, an open card identifies the comment to delete.
|
||||
function M.delete_selected()
|
||||
local open = window.current()
|
||||
local index, selected_id = selected()
|
||||
if vim.bo.buftype == "quickfix" and not index then
|
||||
notify.warn("Not a Scratch Comments list")
|
||||
return
|
||||
end
|
||||
local id
|
||||
if index then
|
||||
id = selected_id
|
||||
else
|
||||
id = open and open.id
|
||||
end
|
||||
if not id then
|
||||
notify.warn(index and "This comment is gone" or "No comment card open")
|
||||
return
|
||||
end
|
||||
if open and open.id == id and vim.bo[vim.api.nvim_win_get_buf(open.comment_win)].modified then
|
||||
notify.warn("Save or discard the comment first")
|
||||
return
|
||||
end
|
||||
local comment = by_id(id)
|
||||
if not comment then
|
||||
notify.warn("This comment is gone")
|
||||
return
|
||||
end
|
||||
comments.delete(comment)
|
||||
notify.info("Deleted comment")
|
||||
if index then
|
||||
refresh(id, index, list_id)
|
||||
elseif open and open.on_saved then
|
||||
open.on_saved()
|
||||
end
|
||||
if open and open.id == id then
|
||||
if index then
|
||||
-- A command run from the list should leave focus there.
|
||||
window.close()
|
||||
else
|
||||
vim.api.nvim_win_close(open.comment_win, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function escape()
|
||||
if window.current() and not window.close() then
|
||||
notify.warn("Save with :w, or discard with :q!")
|
||||
end
|
||||
end
|
||||
|
||||
-- Open the quickfix list without moving the source cursor or showing a card.
|
||||
---@param filter? string only comments matching this
|
||||
function M.open(filter)
|
||||
if not window.close() then
|
||||
notify.warn("Save or discard the comment first")
|
||||
return
|
||||
end
|
||||
local anchored, orphans = views.anchored(), views.orphans()
|
||||
if filter and filter ~= "" then
|
||||
anchored, orphans = matching(anchored, filter), matching(orphans, filter)
|
||||
for bufnr in pairs(pending_jump) do
|
||||
pending_jump[bufnr] = nil
|
||||
end
|
||||
local listed = vim.list_extend(vim.list_slice(anchored), orphans)
|
||||
quickfix.list(anchored, orphans, function(index)
|
||||
local entry = index and listed[index]
|
||||
if not entry then
|
||||
preview.close()
|
||||
elseif entry.snippet then
|
||||
preview.show(card.of(entry))
|
||||
else
|
||||
preview.show(card.of_orphan(entry))
|
||||
active_filter = filter
|
||||
local anchored, orphans = filtered(filter)
|
||||
if #anchored + #orphans == 0 then
|
||||
if list_id and vim.fn.getqflist({ id = 0 }).id == list_id then
|
||||
vim.fn.setqflist({}, "r", { items = {} })
|
||||
vim.cmd.cclose()
|
||||
end
|
||||
end)
|
||||
list_id, list_bufnr = nil, nil
|
||||
notify.info("No comments")
|
||||
return false
|
||||
end
|
||||
list_id = quickfix.list(anchored, orphans)
|
||||
if not list_id then
|
||||
return
|
||||
end
|
||||
list_bufnr = vim.api.nvim_get_current_buf()
|
||||
vim.keymap.set("n", "<CR>", jump, { buffer = list_bufnr, desc = "Jump to comment source" })
|
||||
vim.keymap.set("n", "<Esc>", escape, { buffer = list_bufnr, desc = "Dismiss comment card" })
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -38,6 +38,9 @@ end
|
||||
---@param width integer
|
||||
---@return string
|
||||
function M.fit(text, width)
|
||||
if width <= 0 then
|
||||
return ""
|
||||
end
|
||||
local start = 0
|
||||
while vim.api.nvim_strwidth(vim.fn.strcharpart(text, start)) > width do
|
||||
start = start + 1
|
||||
|
||||
@@ -2,8 +2,12 @@ local M = {}
|
||||
|
||||
---@class ScratchComment
|
||||
---@field id string
|
||||
---@field bufnr integer
|
||||
---@field bufnr? integer
|
||||
---@field extmark_id? integer
|
||||
---@field source_name string
|
||||
---@field last_location ScratchLocation
|
||||
---@field snippet string
|
||||
---@field state? "parked"|"inactive" nil while attached
|
||||
---@field comment string
|
||||
---@field file_path string
|
||||
---@field relative_path string
|
||||
@@ -13,12 +17,15 @@ local M = {}
|
||||
local comments = {}
|
||||
local next_id = 1
|
||||
|
||||
---@param fields { bufnr: integer, comment: string, file_path: string, relative_path: string, charwise?: boolean }
|
||||
---@param fields { bufnr: integer, comment: string, file_path: string, relative_path: string, source_name?: string, last_location?: ScratchLocation, snippet?: string, charwise?: boolean }
|
||||
---@return ScratchComment
|
||||
function M.add(fields)
|
||||
local comment = {
|
||||
id = "comment-" .. next_id,
|
||||
bufnr = fields.bufnr,
|
||||
source_name = fields.source_name or fields.file_path,
|
||||
last_location = fields.last_location or { start_line = 1, end_line = 1 },
|
||||
snippet = fields.snippet or "",
|
||||
comment = fields.comment,
|
||||
file_path = fields.file_path,
|
||||
relative_path = fields.relative_path,
|
||||
|
||||
@@ -46,13 +46,20 @@ end
|
||||
function M.anchored()
|
||||
local views = {}
|
||||
for _, comment in ipairs(store.all()) do
|
||||
local start_line, end_line, start_col, end_col = anchors.range(comment)
|
||||
local start_line, end_line, start_col, end_col
|
||||
if comment.state == "parked" then
|
||||
local where = comment.last_location
|
||||
start_line, end_line = where.start_line, where.end_line
|
||||
start_col, end_col = where.start_col, where.end_col
|
||||
elseif comment.state ~= "inactive" then
|
||||
start_line, end_line, start_col, end_col = anchors.range(comment)
|
||||
end
|
||||
if start_line and end_line then
|
||||
local view = vim.deepcopy(comment) --[[@as ScratchCommentView]]
|
||||
view.start_line, view.end_line = start_line, end_line
|
||||
view.start_col, view.end_col = start_col, end_col
|
||||
view.snippet =
|
||||
table.concat(M.text(comment.bufnr, start_line, end_line, start_col, end_col), "\n")
|
||||
view.snippet = comment.state == "parked" and comment.snippet
|
||||
or table.concat(M.text(comment.bufnr, start_line, end_line, start_col, end_col), "\n")
|
||||
table.insert(views, view)
|
||||
end
|
||||
end
|
||||
@@ -81,7 +88,9 @@ end
|
||||
|
||||
---@return ScratchComment[]
|
||||
function M.orphans()
|
||||
return vim.tbl_filter(anchors.is_orphaned, store.all())
|
||||
return vim.tbl_filter(function(comment)
|
||||
return comment.state == "inactive" or (comment.state == nil and anchors.is_orphaned(comment))
|
||||
end, store.all())
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,92 +0,0 @@
|
||||
local actions = require("scratch_comments.actions")
|
||||
local notify = require("scratch_comments.ui.notify")
|
||||
local views = require("scratch_comments.model.views")
|
||||
local window = require("scratch_comments.ui.window")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param current ScratchCommentWindow
|
||||
---@param direction 1|-1
|
||||
local function cycle(current, direction)
|
||||
if vim.bo[vim.api.nvim_win_get_buf(current.comment_win)].modified then
|
||||
notify.warn("Save or discard the comment first")
|
||||
return
|
||||
end
|
||||
local in_file = views.in_buffer(current.bufnr)
|
||||
if #in_file == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local index = views.index_of(in_file, current.id)
|
||||
local target
|
||||
if index then
|
||||
target = in_file[(index - 1 + direction) % #in_file + 1]
|
||||
elseif direction == 1 then
|
||||
target = vim.iter(in_file):find(function(view)
|
||||
return view.start_line > current.line
|
||||
end) or in_file[1]
|
||||
else
|
||||
target = vim.iter(in_file):rev():find(function(view)
|
||||
return view.start_line < current.line
|
||||
end) or in_file[#in_file]
|
||||
end
|
||||
|
||||
vim.api.nvim_win_close(current.comment_win, true)
|
||||
if vim.api.nvim_win_is_valid(current.source_win) then
|
||||
vim.api.nvim_win_call(current.source_win, function()
|
||||
vim.cmd("normal! m'")
|
||||
vim.api.nvim_win_set_cursor(0, { target.start_line, target.start_col or 0 })
|
||||
end)
|
||||
end
|
||||
actions.show(target, current.source_win)
|
||||
end
|
||||
|
||||
---@param direction 1|-1
|
||||
local function move_cursor(direction)
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local line = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local starts = {}
|
||||
for _, view in ipairs(views.in_buffer(bufnr)) do
|
||||
starts[view.start_line] = true
|
||||
end
|
||||
local lines = vim.tbl_keys(starts)
|
||||
if #lines == 0 then
|
||||
notify.info("No comments in this buffer")
|
||||
return
|
||||
end
|
||||
table.sort(lines)
|
||||
|
||||
local target
|
||||
if direction == 1 then
|
||||
for _, start_line in ipairs(lines) do
|
||||
if start_line > line then
|
||||
target = start_line
|
||||
break
|
||||
end
|
||||
end
|
||||
target = target or lines[1]
|
||||
else
|
||||
for i = #lines, 1, -1 do
|
||||
if lines[i] < line then
|
||||
target = lines[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
target = target or lines[#lines]
|
||||
end
|
||||
|
||||
vim.cmd("normal! m'")
|
||||
vim.api.nvim_win_set_cursor(0, { target, 0 })
|
||||
end
|
||||
|
||||
---@param direction 1|-1
|
||||
function M.jump(direction)
|
||||
local current = window.current()
|
||||
if current then
|
||||
cycle(current, direction)
|
||||
else
|
||||
move_cursor(direction)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,5 +1,13 @@
|
||||
local M = {}
|
||||
|
||||
-- Buffer names may be virtual URIs rather than filesystem paths. Recognize
|
||||
-- the URI scheme, not any particular provider's prefix.
|
||||
---@param name string
|
||||
---@return boolean
|
||||
function M.is_uri(name)
|
||||
return name:match("^%w[%w+.-]*://") ~= nil
|
||||
end
|
||||
|
||||
local function run(args)
|
||||
local result = vim.system(args, { text = true }):wait()
|
||||
if result.code == 0 then
|
||||
|
||||
@@ -4,22 +4,23 @@ local group = "ScratchCommentBackdrop"
|
||||
|
||||
vim.api.nvim_set_hl(0, group, { bg = "#000000", default = true })
|
||||
|
||||
-- Dims the editor behind the comment window. Blending needs true colors, so
|
||||
-- without them there is nothing to dim with.
|
||||
---@param lines? integer rows to cover, from the top (default: the editor's)
|
||||
-- Dim only the editing window, leaving quickfix and other splits readable.
|
||||
-- Blending needs true colors, so without them there is nothing to dim with.
|
||||
---@param parent integer editing window
|
||||
---@return integer? win
|
||||
function M.open(lines)
|
||||
function M.open(parent)
|
||||
if not vim.o.termguicolors then
|
||||
return nil
|
||||
end
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[bufnr].bufhidden = "wipe"
|
||||
local win = vim.api.nvim_open_win(bufnr, false, {
|
||||
relative = "editor",
|
||||
relative = "win",
|
||||
win = parent,
|
||||
row = 0,
|
||||
col = 0,
|
||||
width = vim.o.columns,
|
||||
height = lines or vim.o.lines,
|
||||
width = vim.api.nvim_win_get_width(parent),
|
||||
height = vim.api.nvim_win_get_height(parent),
|
||||
focusable = false,
|
||||
style = "minimal",
|
||||
-- Below the comment window, which opens at the default 50.
|
||||
@@ -33,16 +34,17 @@ function M.open(lines)
|
||||
return win
|
||||
end
|
||||
|
||||
---@param win? integer
|
||||
---@param lines? integer
|
||||
function M.resize(win, lines)
|
||||
if win and vim.api.nvim_win_is_valid(win) then
|
||||
---@param win? integer backdrop window
|
||||
---@param parent integer editing window
|
||||
function M.resize(win, parent)
|
||||
if win and vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_is_valid(parent) then
|
||||
vim.api.nvim_win_set_config(win, {
|
||||
relative = "editor",
|
||||
relative = "win",
|
||||
win = parent,
|
||||
row = 0,
|
||||
col = 0,
|
||||
width = vim.o.columns,
|
||||
height = lines or vim.o.lines,
|
||||
width = vim.api.nvim_win_get_width(parent),
|
||||
height = vim.api.nvim_win_get_height(parent),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,11 +6,14 @@ local M = {}
|
||||
---@param view ScratchCommentView
|
||||
---@return ScratchFrame
|
||||
function M.of(view)
|
||||
local in_file = views.in_buffer(view.bufnr)
|
||||
local in_file = vim.tbl_filter(function(candidate)
|
||||
return candidate.source_name == view.source_name
|
||||
end, views.anchored())
|
||||
return {
|
||||
title = location.title(view.file_path, view),
|
||||
context = vim.split(view.snippet, "\n"),
|
||||
filetype = vim.bo[view.bufnr].filetype,
|
||||
filetype = view.bufnr and vim.api.nvim_buf_is_valid(view.bufnr) and vim.bo[view.bufnr].filetype
|
||||
or "",
|
||||
comment = view.comment,
|
||||
comment_title = ("comment (%d of %d)"):format(views.index_of(in_file, view.id) or 0, #in_file),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
local backdrop = require("scratch_comments.ui.backdrop")
|
||||
local location = require("scratch_comments.location")
|
||||
local notify = require("scratch_comments.ui.notify")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -21,12 +22,9 @@ end
|
||||
---@field filetype string
|
||||
---@field comment string
|
||||
---@field comment_name? string name shown for the editable comment buffer
|
||||
---@field on_save? fun(text: string): boolean? false keeps the text unsaved
|
||||
---@field on_save? fun(text: string): boolean|string? false keeps the text unsaved; "deleted" closes the editor
|
||||
---@field comment_title? string
|
||||
---@field on_close? fun()
|
||||
---@field enter? boolean focus the comment pane (default: true)
|
||||
---@field backdrop? boolean dim the editor behind it (default: true)
|
||||
---@field lines? integer rows to fit in, from the top (default: the editor's)
|
||||
|
||||
---@class ScratchFramePanes
|
||||
---@field context_win integer
|
||||
@@ -34,65 +32,94 @@ end
|
||||
---@field comment_win integer
|
||||
---@field comment_buf integer
|
||||
---@field backdrop_win? integer
|
||||
---@field update fun(spec: ScratchFrame, preserve_comment?: boolean)
|
||||
|
||||
-- Two stacked floats, centered: the commented lines above, the comment below.
|
||||
---@param frame ScratchFrame
|
||||
---@param parent integer editing window to cover, not the quickfix list
|
||||
---@return ScratchFramePanes
|
||||
function M.open(frame)
|
||||
local comment_lines = vim.split(frame.comment, "\n")
|
||||
local wanted_context = #frame.context
|
||||
local wanted_comment = math.max(#comment_lines, 5)
|
||||
|
||||
---@return { width: integer, col: integer, row: integer, context: integer, comment: integer }
|
||||
function M.open(frame, parent)
|
||||
---@return { width: integer, col: integer, row: integer, context: integer, comment: integer, compact: boolean }
|
||||
local function layout()
|
||||
local lines = frame.lines or vim.o.lines
|
||||
local width = math.min(80, vim.o.columns - 4)
|
||||
local context = math.min(wanted_context, math.floor(lines * 0.4))
|
||||
local comment = math.min(wanted_comment, math.floor(lines * 0.3))
|
||||
local lines = vim.api.nvim_win_get_height(parent)
|
||||
local columns = vim.api.nvim_win_get_width(parent)
|
||||
local compact = lines < 6
|
||||
local width = math.max(1, math.min(80, columns - (compact and 0 or 4)))
|
||||
if compact then
|
||||
-- Two bordered panes cannot fit. The comment covers the context pane,
|
||||
-- which stays open so edits and normal resizing keep the same buffers.
|
||||
return {
|
||||
width = width,
|
||||
col = math.floor((columns - width) / 2),
|
||||
row = 0,
|
||||
context = 1,
|
||||
comment = lines,
|
||||
compact = true,
|
||||
}
|
||||
end
|
||||
local available = lines - 4
|
||||
local context = math.max(1, math.min(#frame.context, math.floor(lines * 0.4), available - 1))
|
||||
local comment = math.max(
|
||||
1,
|
||||
math.min(
|
||||
math.max(#vim.split(frame.comment, "\n"), 5),
|
||||
math.floor(lines * 0.3),
|
||||
available - context
|
||||
)
|
||||
)
|
||||
return {
|
||||
width = width,
|
||||
col = math.floor((vim.o.columns - width) / 2),
|
||||
col = math.floor((columns - width) / 2),
|
||||
-- Each pane has a top and a bottom border line.
|
||||
row = math.floor((lines - context - comment - 4) / 2),
|
||||
context = context,
|
||||
comment = comment,
|
||||
compact = false,
|
||||
}
|
||||
end
|
||||
|
||||
local border = (vim.o.winborder == "" or vim.o.winborder == "none") and "rounded"
|
||||
or vim.o.winborder
|
||||
---@param place table
|
||||
---@return vim.api.keyset.win_config context
|
||||
---@return vim.api.keyset.win_config comment
|
||||
local function placement(place)
|
||||
local narrow = place.width < 4
|
||||
return {
|
||||
relative = "editor",
|
||||
relative = "win",
|
||||
win = parent,
|
||||
row = place.row,
|
||||
col = place.col,
|
||||
width = place.width,
|
||||
height = place.context,
|
||||
title = " " .. location.fit(frame.title, place.width - 2) .. " ",
|
||||
height = place.compact and place.comment or place.context,
|
||||
title = (place.compact or narrow) and ""
|
||||
or " " .. location.fit(frame.title, place.width - 2) .. " ",
|
||||
title_pos = "center",
|
||||
border = place.compact and "none" or border,
|
||||
zindex = 45,
|
||||
}, {
|
||||
relative = "editor",
|
||||
row = place.row + place.context + 2,
|
||||
relative = "win",
|
||||
win = parent,
|
||||
row = place.compact and place.row or place.row + place.context + 2,
|
||||
col = place.col,
|
||||
width = place.width,
|
||||
height = place.comment,
|
||||
border = place.compact and "none" or border,
|
||||
zindex = 50,
|
||||
}
|
||||
end
|
||||
|
||||
local dim = frame.backdrop ~= false and backdrop.open(frame.lines) or nil
|
||||
local dim = backdrop.open(parent)
|
||||
local context_place, comment_place = placement(layout())
|
||||
local border = (vim.o.winborder == "" or vim.o.winborder == "none") and "rounded" or nil
|
||||
|
||||
local context_buf = frame_buffer(frame.context, frame.filetype)
|
||||
vim.bo[context_buf].modifiable = false
|
||||
local context_win = vim.api.nvim_open_win(
|
||||
context_buf,
|
||||
false,
|
||||
vim.tbl_extend("error", context_place, { border = border, style = "minimal" })
|
||||
vim.tbl_extend("error", context_place, { style = "minimal" })
|
||||
)
|
||||
|
||||
local comment_buf = frame_buffer(comment_lines, "markdown")
|
||||
local comment_buf = frame_buffer(vim.split(frame.comment, "\n"), "markdown")
|
||||
if frame.comment_name then
|
||||
vim.api.nvim_buf_set_name(
|
||||
comment_buf,
|
||||
@@ -101,11 +128,11 @@ function M.open(frame)
|
||||
end
|
||||
local comment_win = vim.api.nvim_open_win(
|
||||
comment_buf,
|
||||
frame.enter ~= false,
|
||||
true,
|
||||
vim.tbl_extend(
|
||||
"error",
|
||||
comment_place,
|
||||
{ border = border, style = "minimal", title = " comment ", title_pos = "center" }
|
||||
{ style = "minimal", title = " comment ", title_pos = "center" }
|
||||
)
|
||||
)
|
||||
vim.wo[comment_win].wrap = true
|
||||
@@ -126,7 +153,66 @@ function M.open(frame)
|
||||
end
|
||||
|
||||
local group = vim.api.nvim_create_augroup("scratch_comments_frame_" .. comment_win, {})
|
||||
vim.api.nvim_create_autocmd("VimResized", {
|
||||
local tab = vim.api.nvim_win_get_tabpage(parent)
|
||||
local function watch_parent()
|
||||
vim.api.nvim_create_autocmd("WinClosed", {
|
||||
group = group,
|
||||
pattern = tostring(parent),
|
||||
once = true,
|
||||
callback = function()
|
||||
vim.schedule(function()
|
||||
if
|
||||
not (
|
||||
vim.api.nvim_tabpage_is_valid(tab)
|
||||
and vim.api.nvim_win_is_valid(comment_win)
|
||||
and vim.api.nvim_win_is_valid(context_win)
|
||||
)
|
||||
then
|
||||
return
|
||||
end
|
||||
local file_win, other_win, qf_win
|
||||
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tab)) do
|
||||
if vim.api.nvim_win_get_config(win).relative == "" then
|
||||
local kind = vim.bo[vim.api.nvim_win_get_buf(win)].buftype
|
||||
if kind == "" then
|
||||
file_win = file_win or win
|
||||
elseif kind == "quickfix" then
|
||||
qf_win = win
|
||||
else
|
||||
other_win = other_win or win
|
||||
end
|
||||
end
|
||||
end
|
||||
local replacement = file_win or other_win
|
||||
if not replacement and qf_win then
|
||||
if not vim.bo[comment_buf].modified then
|
||||
vim.api.nvim_win_close(comment_win, true)
|
||||
return
|
||||
end
|
||||
-- Preserve unsaved text even if the editor cannot make a split.
|
||||
local ok, created = pcall(vim.api.nvim_win_call, qf_win, function()
|
||||
vim.cmd("aboveleft new")
|
||||
return vim.api.nvim_get_current_win()
|
||||
end)
|
||||
if not ok then
|
||||
notify.warn("No room for an editing window; save or discard the open card")
|
||||
end
|
||||
replacement = ok and created or qf_win
|
||||
end
|
||||
if replacement then
|
||||
parent = replacement
|
||||
backdrop.resize(dim, parent)
|
||||
local context_next, comment_next = placement(layout())
|
||||
vim.api.nvim_win_set_config(context_win, context_next)
|
||||
vim.api.nvim_win_set_config(comment_win, comment_next)
|
||||
watch_parent()
|
||||
end
|
||||
end)
|
||||
end,
|
||||
})
|
||||
end
|
||||
watch_parent()
|
||||
vim.api.nvim_create_autocmd({ "VimResized", "WinResized" }, {
|
||||
group = group,
|
||||
callback = function()
|
||||
if
|
||||
@@ -136,7 +222,10 @@ function M.open(frame)
|
||||
pcall(vim.api.nvim_del_augroup_by_id, group)
|
||||
return
|
||||
end
|
||||
backdrop.resize(dim, frame.lines)
|
||||
if not vim.api.nvim_win_is_valid(parent) then
|
||||
return
|
||||
end
|
||||
backdrop.resize(dim, parent)
|
||||
local context_resized, comment_resized = placement(layout())
|
||||
vim.api.nvim_win_set_config(context_win, context_resized)
|
||||
vim.api.nvim_win_set_config(comment_win, comment_resized)
|
||||
@@ -164,13 +253,37 @@ function M.open(frame)
|
||||
end,
|
||||
})
|
||||
|
||||
return {
|
||||
local panes = {
|
||||
context_win = context_win,
|
||||
context_buf = context_buf,
|
||||
comment_win = comment_win,
|
||||
comment_buf = comment_buf,
|
||||
backdrop_win = dim,
|
||||
}
|
||||
-- A parked comment can become orphaned when its source finishes loading.
|
||||
-- Update the context without discarding any edits to the comment.
|
||||
function panes.update(spec, preserve_comment)
|
||||
frame = spec
|
||||
if preserve_comment then
|
||||
spec.comment = table.concat(vim.api.nvim_buf_get_lines(comment_buf, 0, -1, false), "\n")
|
||||
end
|
||||
vim.bo[context_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(context_buf, 0, -1, false, spec.context)
|
||||
vim.bo[context_buf].modifiable = false
|
||||
vim.bo[context_buf].modified = false
|
||||
vim.bo[context_buf].filetype = spec.filetype
|
||||
if not preserve_comment then
|
||||
vim.bo[comment_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(comment_buf, 0, -1, false, vim.split(spec.comment, "\n"))
|
||||
vim.bo[comment_buf].modified = false
|
||||
vim.bo[comment_buf].modifiable = false
|
||||
end
|
||||
backdrop.resize(dim, parent)
|
||||
local context_next, comment_next = placement(layout())
|
||||
vim.api.nvim_win_set_config(context_win, context_next)
|
||||
vim.api.nvim_win_set_config(comment_win, comment_next)
|
||||
end
|
||||
return panes
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,50 +0,0 @@
|
||||
local frame = require("scratch_comments.ui.frame")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type integer[] the panes of the open preview
|
||||
local current = {}
|
||||
|
||||
---@type ScratchFrame? what the open preview shows
|
||||
local showing
|
||||
|
||||
function M.close()
|
||||
for _, win in ipairs(current) do
|
||||
pcall(vim.api.nvim_win_close, win, true)
|
||||
end
|
||||
current = {}
|
||||
showing = nil
|
||||
end
|
||||
|
||||
-- A card beside whatever you are browsing: no focus, no editing.
|
||||
---@param spec ScratchFrame
|
||||
function M.show(spec)
|
||||
local browsing = vim.api.nvim_get_current_win()
|
||||
M.close()
|
||||
spec.enter = false
|
||||
-- Fit above the window being browsed, so the card does not cover it, and
|
||||
-- dim only that area so the window stays readable.
|
||||
local above = vim.fn.win_screenpos(browsing)[1] - 1
|
||||
spec.lines = above >= 10 and above or nil
|
||||
spec.backdrop = spec.lines ~= nil
|
||||
local panes = frame.open(spec)
|
||||
vim.bo[panes.comment_buf].modifiable = false
|
||||
current = { panes.comment_win, panes.context_win, panes.backdrop_win }
|
||||
showing = spec
|
||||
|
||||
-- The window being browsed moves when the editor resizes, so measure again.
|
||||
local group = vim.api.nvim_create_augroup("scratch_comments_preview", { clear = true })
|
||||
vim.api.nvim_create_autocmd("VimResized", {
|
||||
group = group,
|
||||
callback = function()
|
||||
if not showing then
|
||||
return true
|
||||
end
|
||||
vim.api.nvim_win_call(browsing, function()
|
||||
M.show(showing)
|
||||
end)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -9,54 +9,15 @@ local function summary(text)
|
||||
end
|
||||
|
||||
-- Quickfix rather than a picker: any quickfix front-end (Trouble, Snacks,
|
||||
-- Telescope, nvim-bqf) can display it.
|
||||
-- Reports the entry the cursor is on, and nil once the list is gone.
|
||||
---@param on_entry? fun(index: integer?)
|
||||
local function follow(on_entry)
|
||||
if not on_entry then
|
||||
return
|
||||
end
|
||||
local group = vim.api.nvim_create_augroup("scratch_comments_quickfix", { clear = true })
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
vim.api.nvim_create_autocmd({ "CursorMoved", "BufEnter" }, {
|
||||
group = group,
|
||||
buffer = bufnr,
|
||||
callback = function()
|
||||
on_entry(vim.api.nvim_win_get_cursor(0)[1])
|
||||
end,
|
||||
})
|
||||
for _, event in ipairs({ "BufLeave", "BufWipeout" }) do
|
||||
vim.api.nvim_create_autocmd(event, {
|
||||
group = group,
|
||||
buffer = bufnr,
|
||||
callback = function()
|
||||
on_entry(nil)
|
||||
end,
|
||||
})
|
||||
end
|
||||
vim.api.nvim_create_autocmd("WinClosed", {
|
||||
group = group,
|
||||
pattern = tostring(vim.api.nvim_get_current_win()),
|
||||
once = true,
|
||||
callback = function()
|
||||
on_entry(nil)
|
||||
end,
|
||||
})
|
||||
on_entry(vim.api.nvim_win_get_cursor(0)[1])
|
||||
end
|
||||
|
||||
-- Telescope, nvim-bqf) can display the entries.
|
||||
---@param views ScratchCommentView[]
|
||||
---@param orphans ScratchComment[]
|
||||
---@param on_entry? fun(index: integer?) called with the entry the cursor is on
|
||||
function M.list(views, orphans, on_entry)
|
||||
if #views + #orphans == 0 then
|
||||
notify.info("No comments")
|
||||
return
|
||||
end
|
||||
|
||||
---@return table[]
|
||||
function M.items(views, orphans)
|
||||
local entries = vim.tbl_map(function(comment)
|
||||
return {
|
||||
filename = comment.file_path,
|
||||
filename = comment.source_name,
|
||||
user_data = { scratch_comments_id = comment.id },
|
||||
lnum = comment.start_line,
|
||||
end_lnum = comment.end_line,
|
||||
col = comment.start_col and comment.start_col + 1,
|
||||
@@ -65,15 +26,27 @@ function M.list(views, orphans, on_entry)
|
||||
}
|
||||
end, views)
|
||||
for _, comment in ipairs(orphans) do
|
||||
table.insert(
|
||||
entries,
|
||||
{ filename = comment.file_path, text = "[orphaned] " .. summary(comment.comment) }
|
||||
)
|
||||
table.insert(entries, {
|
||||
filename = comment.source_name,
|
||||
user_data = { scratch_comments_id = comment.id },
|
||||
text = "[orphaned] " .. summary(comment.comment),
|
||||
})
|
||||
end
|
||||
|
||||
vim.fn.setqflist({}, " ", { title = "Comments", items = entries })
|
||||
return entries
|
||||
end
|
||||
|
||||
---@param views ScratchCommentView[]
|
||||
---@param orphans ScratchComment[]
|
||||
function M.list(views, orphans)
|
||||
if #views + #orphans == 0 then
|
||||
notify.info("No comments")
|
||||
return
|
||||
end
|
||||
|
||||
vim.fn.setqflist({}, " ", { title = "Comments", items = M.items(views, orphans) })
|
||||
vim.cmd.copen()
|
||||
follow(on_entry)
|
||||
return vim.fn.getqflist({ id = 0 }).id
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,6 +1,5 @@
|
||||
local frame = require("scratch_comments.ui.frame")
|
||||
local notify = require("scratch_comments.ui.notify")
|
||||
local preview = require("scratch_comments.ui.preview")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -16,26 +15,79 @@ local mode_names = {
|
||||
|
||||
---@class ScratchCommentWindow
|
||||
---@field id? string unset until a new comment is saved
|
||||
---@field bufnr integer
|
||||
---@field bufnr? integer
|
||||
---@field source_name? string
|
||||
---@field line integer
|
||||
---@field source_win integer
|
||||
---@field comment_win? integer
|
||||
---@field on_close? fun()
|
||||
---@field on_saved? fun()
|
||||
---@field update? fun(spec: ScratchFrame)
|
||||
|
||||
---@type ScratchCommentWindow?
|
||||
local current
|
||||
|
||||
-- A quickfix row with no source location still edits in the file area, while
|
||||
-- closing its card returns focus to the list.
|
||||
---@param record ScratchCommentWindow
|
||||
---@return integer?
|
||||
local function editing_window(record)
|
||||
local source = record.source_win
|
||||
if
|
||||
vim.api.nvim_win_is_valid(source)
|
||||
and vim.bo[vim.api.nvim_win_get_buf(source)].buftype ~= "quickfix"
|
||||
then
|
||||
return source
|
||||
end
|
||||
local file_win, other_win
|
||||
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||
if vim.api.nvim_win_get_config(win).relative == "" then
|
||||
local bufnr = vim.api.nvim_win_get_buf(win)
|
||||
if record.source_name and vim.api.nvim_buf_get_name(bufnr) == record.source_name then
|
||||
return win
|
||||
end
|
||||
if vim.bo[bufnr].buftype == "" then
|
||||
file_win = file_win or win
|
||||
elseif vim.bo[bufnr].buftype ~= "quickfix" then
|
||||
other_win = other_win or win
|
||||
end
|
||||
end
|
||||
end
|
||||
if file_win or other_win then
|
||||
return file_win or other_win
|
||||
end
|
||||
-- Quickfix cannot provide a source area on its own. Make one if there is
|
||||
-- room; otherwise keep the list usable rather than drawing over it.
|
||||
if not pcall(function()
|
||||
vim.cmd("aboveleft new")
|
||||
end) then
|
||||
notify.warn("Make room for an editing window to open this comment")
|
||||
return nil
|
||||
end
|
||||
return vim.api.nvim_get_current_win()
|
||||
end
|
||||
|
||||
---@param spec ScratchFrame
|
||||
---@param record ScratchCommentWindow
|
||||
function M.open(spec, record)
|
||||
preview.close()
|
||||
spec.on_close = function()
|
||||
if current and current.comment_win == record.comment_win then
|
||||
current = nil
|
||||
end
|
||||
if record.on_close then
|
||||
record.on_close()
|
||||
end
|
||||
end
|
||||
local source_name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(record.bufnr), ":t")
|
||||
local source_buffer_name = record.source_name
|
||||
or (record.bufnr and vim.api.nvim_buf_get_name(record.bufnr))
|
||||
or "comment"
|
||||
local source_name = vim.fn.fnamemodify(source_buffer_name, ":t")
|
||||
spec.comment_name = source_name .. ":" .. record.line .. " [comment]"
|
||||
local panes = frame.open(spec)
|
||||
local parent = editing_window(record)
|
||||
if not parent then
|
||||
return
|
||||
end
|
||||
local panes = frame.open(spec, parent)
|
||||
local comment_win, comment_buf = panes.comment_win, panes.comment_buf
|
||||
record.comment_win = comment_win
|
||||
current = record
|
||||
@@ -51,6 +103,13 @@ function M.open(spec, record)
|
||||
vim.cmd.redraw()
|
||||
end
|
||||
end
|
||||
record.update = function(next_spec)
|
||||
next_spec.on_close = spec.on_close
|
||||
next_spec.on_save = spec.on_save
|
||||
panes.update(next_spec, true)
|
||||
spec = next_spec
|
||||
show_mode()
|
||||
end
|
||||
show_mode()
|
||||
vim.api.nvim_create_autocmd("ModeChanged", { buffer = comment_buf, callback = show_mode })
|
||||
|
||||
@@ -73,10 +132,21 @@ function M.open(spec, record)
|
||||
callback = function()
|
||||
local lines = vim.api.nvim_buf_get_lines(comment_buf, 0, -1, false)
|
||||
local text = vim.trim(table.concat(lines, "\n"))
|
||||
if text ~= "" and spec.on_save and spec.on_save(text) == false then
|
||||
local result = spec.on_save and spec.on_save(text)
|
||||
if result == false then
|
||||
return
|
||||
end
|
||||
vim.bo[comment_buf].modified = false
|
||||
if record.on_saved then
|
||||
record.on_saved()
|
||||
end
|
||||
if result == "deleted" then
|
||||
vim.schedule(function()
|
||||
if vim.api.nvim_win_is_valid(comment_win) then
|
||||
vim.api.nvim_win_close(comment_win, true)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
@@ -91,6 +161,9 @@ function M.close()
|
||||
if vim.bo[vim.api.nvim_win_get_buf(open.comment_win)].modified then
|
||||
return false
|
||||
end
|
||||
-- A command replacing the card (e.g. :CommentExport!) owns the next focus.
|
||||
-- Only a user closing the card should return to its source or list.
|
||||
open.on_close = nil
|
||||
pcall(vim.api.nvim_win_close, open.comment_win, true)
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -11,23 +11,11 @@ local command = vim.api.nvim_create_user_command
|
||||
|
||||
command("Comment", function(ctx)
|
||||
require("scratch_comments.actions").comment(ctx.line1, ctx.line2, ctx.range == 2)
|
||||
end, { range = true, desc = "Comment on the current line or range" })
|
||||
|
||||
command("CommentShow", function()
|
||||
scratch().show()
|
||||
end, { desc = "Show the comments at the cursor" })
|
||||
end, { range = true, desc = "Add or edit a source comment, or edit the selected quickfix comment" })
|
||||
|
||||
command("CommentDelete", function()
|
||||
scratch().delete()
|
||||
end, { desc = "Delete the comment at the cursor" })
|
||||
|
||||
command("CommentNext", function()
|
||||
scratch().next()
|
||||
end, { desc = "Go to the next comment" })
|
||||
|
||||
command("CommentPrev", function()
|
||||
scratch().prev()
|
||||
end, { desc = "Go to the previous comment" })
|
||||
end, { desc = "Delete the selected quickfix comment or open card" })
|
||||
|
||||
command("CommentList", function(ctx)
|
||||
scratch().list(ctx.args ~= "" and ctx.args or nil)
|
||||
|
||||
@@ -127,11 +127,9 @@ describe("an orphan", function()
|
||||
expect.equality(only().start_line, 3)
|
||||
end)
|
||||
|
||||
it("is offered by :CommentDelete on a line without comments", function()
|
||||
helpers.pick(1)
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
vim.cmd("CommentDelete")
|
||||
expect.equality(#helpers.offered, 1)
|
||||
it("can be deleted from the comment list", function()
|
||||
vim.cmd.CommentList()
|
||||
vim.cmd.CommentDelete()
|
||||
expect.equality(#views.orphans(), 0)
|
||||
end)
|
||||
end)
|
||||
@@ -148,8 +146,105 @@ describe("a file's comments", function()
|
||||
expect.equality(#views.anchored(), 1)
|
||||
end)
|
||||
|
||||
it("go with :bd", function()
|
||||
it("survive :bd and reattach when reopened", function()
|
||||
vim.cmd("bdelete")
|
||||
expect.equality(#store.all(), 1)
|
||||
expect.equality(store.all()[1].state, "parked")
|
||||
expect.equality(views.anchored()[1].comment, "on the first line")
|
||||
vim.cmd.edit("tests/fixtures/lifecycle.txt")
|
||||
vim.wait(0)
|
||||
expect.equality(store.all()[1].state, nil)
|
||||
expect.equality(views.anchored()[1].start_line, 1)
|
||||
end)
|
||||
|
||||
it("retains a comment through :bunload and :bwipeout", function()
|
||||
local source = vim.api.nvim_get_current_buf()
|
||||
vim.cmd.enew()
|
||||
vim.cmd("bunload tests/fixtures/lifecycle.txt")
|
||||
expect.equality(store.all()[1].state, "parked")
|
||||
local ns = vim.api.nvim_get_namespaces().scratch_comments
|
||||
expect.equality(#vim.api.nvim_buf_get_extmarks(source, ns, 0, -1, {}), 0)
|
||||
expect.equality(#views.anchored(), 1)
|
||||
vim.cmd("bwipeout! tests/fixtures/lifecycle.txt")
|
||||
expect.equality(#store.all(), 1)
|
||||
expect.equality(store.all()[1].state, "parked")
|
||||
end)
|
||||
|
||||
it("keeps a wiped diff-style buffer until the same URI returns", function()
|
||||
local url = "codediff:////tmp/repo///HEAD/review.lua"
|
||||
vim.cmd.enew()
|
||||
vim.api.nvim_buf_set_name(0, url)
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "one", "two" })
|
||||
vim.bo.bufhidden = "wipe"
|
||||
vim.cmd("2Comment")
|
||||
helpers.write("virtual note")
|
||||
local old = vim.api.nvim_get_current_buf()
|
||||
vim.cmd("enew!")
|
||||
expect.equality(vim.api.nvim_buf_is_valid(old), false)
|
||||
expect.equality(store.all()[#store.all()].state, "parked")
|
||||
vim.api.nvim_buf_set_name(0, url)
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "one", "two" })
|
||||
vim.api.nvim_exec_autocmds("BufEnter", { buffer = 0 })
|
||||
vim.wait(0)
|
||||
expect.equality(store.all()[#store.all()].state, nil)
|
||||
expect.equality(helpers.sign_at(2), "│")
|
||||
expect.equality(store.all()[#store.all()].comment, "virtual note")
|
||||
expect.equality(#views.anchored(), 2)
|
||||
end)
|
||||
|
||||
it("waits for a delayed URI read before reattaching", function()
|
||||
local url = "review-test:///later.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, { "one", "two" })
|
||||
vim.bo.bufhidden = "wipe"
|
||||
vim.cmd("2Comment")
|
||||
helpers.write("delayed")
|
||||
vim.cmd("enew!")
|
||||
expect.equality(store.all()[#store.all()].state, "parked")
|
||||
vim.cmd.edit(url)
|
||||
vim.wait(0)
|
||||
expect.equality(store.all()[#store.all()].state, "parked")
|
||||
expect.equality(pending, vim.api.nvim_get_current_buf())
|
||||
vim.api.nvim_buf_set_lines(pending, 0, -1, false, { "one" })
|
||||
vim.wait(0)
|
||||
expect.equality(store.all()[#store.all()].state, "parked")
|
||||
vim.api.nvim_buf_set_lines(pending, 1, -1, false, { "two" })
|
||||
vim.wait(0)
|
||||
expect.equality(store.all()[#store.all()].state, nil)
|
||||
expect.equality(views.in_buffer(pending)[1].start_line, 2)
|
||||
-- A provider may replace an already-matching partial result again.
|
||||
vim.api.nvim_buf_set_lines(pending, 1, 2, false, { "two" })
|
||||
vim.wait(0)
|
||||
expect.equality(store.all()[#store.all()].state, nil)
|
||||
expect.equality(views.in_buffer(pending)[1].start_line, 2)
|
||||
end)
|
||||
|
||||
it("marks a changed saved range inactive after reopening", function()
|
||||
vim.cmd("bdelete")
|
||||
vim.cmd.edit("tests/fixtures/lifecycle.txt")
|
||||
vim.api.nvim_buf_set_lines(0, 0, 1, false, { "different" })
|
||||
vim.wait(0)
|
||||
expect.equality(#views.orphans(), 1)
|
||||
expect.equality(#views.anchored(), 0)
|
||||
vim.cmd.CommentList()
|
||||
vim.cmd.CommentDelete()
|
||||
expect.equality(#store.all(), 0)
|
||||
end)
|
||||
|
||||
it("retains an orphan when its buffer is wiped", function()
|
||||
vim.cmd("1d")
|
||||
helpers.text_changed()
|
||||
expect.equality(#views.orphans(), 1)
|
||||
vim.cmd("bwipeout!")
|
||||
expect.equality(#views.orphans(), 1)
|
||||
end)
|
||||
end)
|
||||
@@ -19,11 +19,12 @@ describe(":Comment", function()
|
||||
expect.equality(views.anchored()[1].comment, "first comment")
|
||||
end)
|
||||
|
||||
it("on the same line edits that comment instead of adding one", function()
|
||||
it("opens an existing comment on the exact range for editing", function()
|
||||
vim.cmd("Comment")
|
||||
helpers.write("first comment")
|
||||
vim.cmd("Comment")
|
||||
expect.equality(helpers.text(), "first comment")
|
||||
expect.equality(vim.bo.modifiable, true)
|
||||
helpers.write("edited comment")
|
||||
expect.equality(#store.all(), 1)
|
||||
expect.equality(views.anchored()[1].comment, "edited comment")
|
||||
@@ -72,6 +73,26 @@ describe("the comment window", function()
|
||||
expect.equality(store.all()[1].comment, "first line\nsecond line, revised")
|
||||
end)
|
||||
|
||||
it("adds nothing when a new empty comment is saved", function()
|
||||
vim.cmd("Comment")
|
||||
vim.cmd.stopinsert()
|
||||
vim.cmd.write()
|
||||
vim.cmd.quit()
|
||||
expect.equality(#store.all(), 0)
|
||||
end)
|
||||
|
||||
it("deletes a comment when saved empty after its first write", function()
|
||||
vim.cmd("Comment")
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "first" })
|
||||
vim.cmd.write()
|
||||
expect.equality(#store.all(), 1)
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "" })
|
||||
vim.cmd.write()
|
||||
vim.wait(0)
|
||||
expect.equality(#store.all(), 0)
|
||||
expect.equality(#helpers.floats(), 0)
|
||||
end)
|
||||
|
||||
it("adds nothing when discarded with :q!", function()
|
||||
vim.cmd("Comment")
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "hello" })
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -103,6 +104,518 @@ describe(":CommentList", function()
|
||||
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()
|
||||
vim.api.nvim_set_current_win(file_win)
|
||||
vim.wait(0)
|
||||
expect.equality(vim.api.nvim_get_current_win(), file_win)
|
||||
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("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("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")
|
||||
@@ -156,103 +669,187 @@ describe(":CommentList with a filter", function()
|
||||
end)
|
||||
|
||||
describe("one card at a time", function()
|
||||
it(":CommentList replaces an open comment window", function()
|
||||
vim.cmd("CommentShow")
|
||||
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(), 2)
|
||||
expect.equality(#helpers.floats(), 0)
|
||||
expect.equality(vim.bo.buftype, "quickfix")
|
||||
end)
|
||||
|
||||
it(":CommentList keeps an unsaved comment, and says so", function()
|
||||
vim.cmd("CommentShow")
|
||||
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("CommentShow")
|
||||
vim.cmd.CommentList()
|
||||
vim.cmd.Comment()
|
||||
vim.cmd("CommentExport!")
|
||||
expect.equality(#helpers.every_float(), 0)
|
||||
expect.equality(vim.bo.buftype, "nofile")
|
||||
end)
|
||||
|
||||
it("a comment window replaces the list's card", function()
|
||||
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("the card beside the list", function()
|
||||
---@return string
|
||||
local function card()
|
||||
local panes = vim.tbl_filter(function(win)
|
||||
return vim.bo[vim.api.nvim_win_get_buf(win)].filetype == "markdown"
|
||||
end, helpers.floats())
|
||||
return panes[1] and helpers.text(vim.api.nvim_win_get_buf(panes[1])) or ""
|
||||
end
|
||||
|
||||
---@param line integer
|
||||
local function move_to(line)
|
||||
vim.api.nvim_win_set_cursor(0, { line, 0 })
|
||||
vim.api.nvim_exec_autocmds("CursorMoved", { buffer = vim.api.nvim_get_current_buf() })
|
||||
end
|
||||
|
||||
it("shows the comment the cursor is on, and follows it", function()
|
||||
vim.cmd("CommentList")
|
||||
expect.equality(card(), "on a")
|
||||
move_to(2)
|
||||
expect.equality(card(), "on c")
|
||||
move_to(1)
|
||||
expect.equality(card(), "on a")
|
||||
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("dims what is above it, leaving the list readable", function()
|
||||
vim.cmd("CommentList")
|
||||
local dim = vim.tbl_filter(function(win)
|
||||
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 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, helpers.every_float())
|
||||
expect.equality(#dim, 1)
|
||||
local config = vim.api.nvim_win_get_config(dim[1])
|
||||
expect.equality(config.height < vim.o.lines, true)
|
||||
expect.equality(config.height < vim.fn.win_screenpos(0)[1], true)
|
||||
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)
|
||||
|
||||
---@return vim.api.keyset.win_config
|
||||
local function card_config()
|
||||
local panes = vim.tbl_filter(function(win)
|
||||
return vim.bo[vim.api.nvim_win_get_buf(win)].filetype == "markdown"
|
||||
end, helpers.floats())
|
||||
return vim.api.nvim_win_get_config(panes[1])
|
||||
end
|
||||
|
||||
it("is measured again when the editor resizes", function()
|
||||
local columns, lines = vim.o.columns, vim.o.lines
|
||||
vim.o.lines = 44
|
||||
vim.cmd("CommentList")
|
||||
local before = card_config()
|
||||
vim.o.lines = 24
|
||||
vim.api.nvim_exec_autocmds("VimResized", {})
|
||||
local after = card_config()
|
||||
-- A shorter editor leaves less room above the list, so the card moves up
|
||||
-- instead of covering it.
|
||||
expect.equality(after.row < before.row, true)
|
||||
expect.equality(after.row + after.height + 1 <= vim.fn.win_screenpos(0)[1], true)
|
||||
vim.o.columns, vim.o.lines = columns, lines
|
||||
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 the focus in the list", function()
|
||||
vim.cmd("CommentList")
|
||||
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("goes away with the list", function()
|
||||
vim.cmd("CommentList")
|
||||
vim.api.nvim_exec_autocmds("BufLeave", { buffer = vim.api.nvim_get_current_buf() })
|
||||
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)
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
local helpers = require("helpers")
|
||||
local expect = MiniTest.expect
|
||||
|
||||
local file_win
|
||||
|
||||
local function cursor_line()
|
||||
return vim.api.nvim_win_get_cursor(file_win)[1]
|
||||
end
|
||||
|
||||
---@param command string
|
||||
---@return string lines the cursor visited, four steps
|
||||
local function walk(command)
|
||||
local lines = {}
|
||||
for _ = 1, 4 do
|
||||
vim.cmd(command)
|
||||
table.insert(lines, cursor_line())
|
||||
end
|
||||
return table.concat(lines, ",")
|
||||
end
|
||||
|
||||
before_each(function()
|
||||
helpers.reset()
|
||||
helpers.buffer("notes.md", { "1", "2", "3", "4", "5", "6", "7", "8" })
|
||||
file_win = vim.api.nvim_get_current_win()
|
||||
vim.cmd("2Comment")
|
||||
helpers.write("on two")
|
||||
vim.cmd("4,5Comment")
|
||||
helpers.write("on four and five")
|
||||
vim.cmd("4Comment")
|
||||
helpers.write("also on four")
|
||||
vim.cmd("7Comment")
|
||||
helpers.write("on seven")
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
end)
|
||||
|
||||
describe("without a comment open", function()
|
||||
it(":CommentNext visits each start line once, then wraps", function()
|
||||
expect.equality(walk("CommentNext"), "2,4,7,2")
|
||||
end)
|
||||
|
||||
it(":CommentPrev goes back, then wraps", function()
|
||||
expect.equality(walk("CommentPrev"), "7,4,2,7")
|
||||
end)
|
||||
|
||||
it("jumps can be undone with <C-o>", function()
|
||||
vim.cmd("CommentNext")
|
||||
vim.cmd([[execute "normal! \<C-o>"]])
|
||||
expect.equality(cursor_line(), 1)
|
||||
end)
|
||||
|
||||
it("only moves the cursor", function()
|
||||
vim.cmd("CommentNext")
|
||||
expect.equality(#helpers.floats(), 0)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("with a comment open", function()
|
||||
before_each(function()
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
vim.cmd("CommentShow")
|
||||
end)
|
||||
|
||||
it("counts the file's comments", function()
|
||||
expect.equality(helpers.title(0), " comment (1 of 4) [NORMAL] ")
|
||||
end)
|
||||
|
||||
it(":CommentNext shows each comment in turn, then wraps, the cursor following", function()
|
||||
local shown, lines = {}, {}
|
||||
for _ = 1, 4 do
|
||||
vim.cmd("CommentNext")
|
||||
table.insert(shown, helpers.text())
|
||||
table.insert(lines, cursor_line())
|
||||
end
|
||||
expect.equality(table.concat(shown, ","), "also on four,on four and five,on seven,on two")
|
||||
expect.equality(table.concat(lines, ","), "4,4,7,2")
|
||||
expect.equality(#helpers.floats(), 2)
|
||||
expect.equality(helpers.title(0), " comment (1 of 4) [NORMAL] ")
|
||||
end)
|
||||
|
||||
it(":CommentPrev goes back", function()
|
||||
vim.cmd("CommentPrev")
|
||||
expect.equality(helpers.text(), "on seven")
|
||||
end)
|
||||
|
||||
it("won't move while the comment has unsaved changes", function()
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "unsaved" })
|
||||
vim.cmd("CommentNext")
|
||||
expect.equality(helpers.text(), "unsaved")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("from a new comment", function()
|
||||
it("moves to the next comment after its line", function()
|
||||
vim.api.nvim_win_set_cursor(0, { 3, 0 })
|
||||
vim.cmd("Comment")
|
||||
vim.cmd("stopinsert")
|
||||
vim.cmd("CommentNext")
|
||||
expect.equality(helpers.text(), "also on four")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("with comments in another file", function()
|
||||
it("stays in the current file", function()
|
||||
local other = helpers.buffer("other.md", { "1", "2", "3" })
|
||||
vim.cmd("3Comment")
|
||||
helpers.write("in the other file")
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
vim.cmd("CommentNext")
|
||||
vim.cmd("CommentNext")
|
||||
expect.equality(vim.api.nvim_get_current_buf(), other)
|
||||
expect.equality(vim.api.nvim_win_get_cursor(0)[1], 3)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("when things change under an open comment", function()
|
||||
it("moves on from its line when its comment was deleted", function()
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
vim.cmd("CommentShow")
|
||||
vim.api.nvim_set_current_win(file_win)
|
||||
vim.cmd("CommentDelete")
|
||||
vim.cmd("CommentNext")
|
||||
expect.equality(helpers.text(), "also on four")
|
||||
end)
|
||||
|
||||
it("still cycles when the window it came from is closed", function()
|
||||
vim.cmd("split")
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
local split = vim.api.nvim_get_current_win()
|
||||
vim.cmd("CommentShow")
|
||||
vim.api.nvim_win_close(split, true)
|
||||
vim.cmd("CommentNext")
|
||||
expect.equality(helpers.text(), "also on four")
|
||||
end)
|
||||
end)
|
||||
@@ -21,7 +21,14 @@ end
|
||||
|
||||
describe("the plugin", function()
|
||||
it("defines its commands without setup()", function()
|
||||
expect.equality(fresh("io.write(vim.fn.exists(':CommentShow'))"), "2")
|
||||
expect.equality(fresh("io.write(vim.fn.exists(':CommentList'))"), "2")
|
||||
end)
|
||||
|
||||
it("defines :CommentDelete but not the old navigation commands", function()
|
||||
expect.equality(fresh("io.write(vim.fn.exists(':CommentDelete'))"), "2")
|
||||
for _, command in ipairs({ "CommentShow", "CommentNext", "CommentPrev" }) do
|
||||
expect.equality(fresh("io.write(vim.fn.exists(':" .. command .. "'))"), "0")
|
||||
end
|
||||
end)
|
||||
|
||||
it("loads nothing until a command runs", function()
|
||||
|
||||
@@ -10,88 +10,153 @@ before_each(function()
|
||||
helpers.write("on one")
|
||||
vim.cmd("1,3Comment")
|
||||
helpers.write("on one to three")
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
end)
|
||||
|
||||
describe(":CommentShow", function()
|
||||
it("asks which comment when several cover the line", function()
|
||||
helpers.pick(2)
|
||||
vim.cmd("CommentShow")
|
||||
expect.equality(#helpers.offered, 2)
|
||||
expect.equality(helpers.text(), "on one to three")
|
||||
end)
|
||||
|
||||
it("names each comment by its location and first line", function()
|
||||
helpers.pick(0)
|
||||
vim.cmd("CommentShow")
|
||||
expect.equality(helpers.labels, { "line 1: on one", "lines 1–3: on one to three" })
|
||||
end)
|
||||
|
||||
it("saves edits to the comment it shows", function()
|
||||
helpers.pick(2)
|
||||
vim.cmd("CommentShow")
|
||||
helpers.write("picked the range")
|
||||
expect.equality(#store.all(), 2)
|
||||
expect.equality(views.anchored()[2].comment, "picked the range")
|
||||
end)
|
||||
|
||||
it("opens nothing when the picker is cancelled", function()
|
||||
helpers.pick(0)
|
||||
vim.cmd("CommentShow")
|
||||
expect.equality(#helpers.floats(), 0)
|
||||
end)
|
||||
|
||||
it("closes with :q", function()
|
||||
helpers.pick(2)
|
||||
vim.cmd("CommentShow")
|
||||
vim.cmd("quit")
|
||||
expect.equality(#helpers.floats(), 0)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("a comment that is gone", function()
|
||||
it("keeps your text and says nothing was saved", function()
|
||||
helpers.pick(1)
|
||||
vim.cmd("CommentShow")
|
||||
vim.cmd("CommentClear")
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "typed after it was cleared" })
|
||||
vim.cmd("write")
|
||||
expect.equality(vim.bo.modified, true)
|
||||
expect.equality(#helpers.floats(), 2)
|
||||
expect.equality(
|
||||
helpers.notified()[#helpers.notified()],
|
||||
"This comment is gone; nothing was saved"
|
||||
)
|
||||
end)
|
||||
vim.cmd.CommentList()
|
||||
end)
|
||||
|
||||
describe(":CommentDelete", function()
|
||||
it("deletes the picked comment", function()
|
||||
helpers.pick(1)
|
||||
vim.cmd("CommentDelete")
|
||||
it("deletes the cursor row without opening a card or jumping", function()
|
||||
local qf_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_cursor(qf_win, { 2, 0 })
|
||||
expect.equality(vim.fn.getqflist({ idx = 0 }).idx, 1)
|
||||
vim.cmd.CommentDelete()
|
||||
expect.equality(vim.api.nvim_get_current_win(), qf_win)
|
||||
expect.equality(#store.all(), 1)
|
||||
expect.equality(views.anchored()[1].comment, "on one")
|
||||
expect.equality(#vim.fn.getqflist(), 1)
|
||||
end)
|
||||
|
||||
it("closes quickfix after deleting its last row", function()
|
||||
local qf_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_cursor(qf_win, { 2, 0 })
|
||||
vim.cmd.CommentDelete()
|
||||
vim.cmd.CommentDelete()
|
||||
expect.equality(#store.all(), 0)
|
||||
expect.equality(vim.api.nvim_win_is_valid(qf_win), false)
|
||||
end)
|
||||
|
||||
it("deletes the focused card by its ID", function()
|
||||
vim.cmd.Comment()
|
||||
vim.cmd.CommentDelete()
|
||||
vim.wait(0)
|
||||
expect.equality(#store.all(), 1)
|
||||
expect.equality(views.anchored()[1].comment, "on one to three")
|
||||
end)
|
||||
|
||||
it("deletes nothing when the picker is cancelled", function()
|
||||
helpers.pick(0)
|
||||
vim.cmd("CommentDelete")
|
||||
expect.equality(#store.all(), 2)
|
||||
it("deletes the cursor row rather than a different open card", function()
|
||||
local qf_win = vim.api.nvim_get_current_win()
|
||||
vim.cmd.Comment()
|
||||
local open = assert(require("scratch_comments.ui.window").current())
|
||||
vim.api.nvim_set_current_win(qf_win)
|
||||
vim.api.nvim_win_set_cursor(qf_win, { 2, 0 })
|
||||
vim.cmd.CommentDelete()
|
||||
expect.equality(vim.api.nvim_get_current_win(), qf_win)
|
||||
expect.equality(views.anchored()[1].comment, "on one")
|
||||
expect.equality(require("scratch_comments.ui.window").current(), open)
|
||||
end)
|
||||
|
||||
it("says so on a line with no comment and no orphans", function()
|
||||
vim.api.nvim_win_set_cursor(0, { 4, 0 })
|
||||
vim.cmd("CommentDelete")
|
||||
it("keeps another open card's unsaved edits when deleting a quickfix row", function()
|
||||
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.CommentDelete()
|
||||
expect.equality(views.anchored()[1].comment, "on one")
|
||||
expect.equality(vim.bo[vim.api.nvim_win_get_buf(card_win)].modified, true)
|
||||
vim.api.nvim_set_current_win(card_win)
|
||||
vim.cmd("q!")
|
||||
end)
|
||||
|
||||
it("keeps quickfix focus when invoked there while an editor is open", function()
|
||||
local qf_win = vim.api.nvim_get_current_win()
|
||||
vim.cmd.Comment()
|
||||
vim.api.nvim_set_current_win(qf_win)
|
||||
vim.cmd.CommentDelete()
|
||||
vim.wait(0)
|
||||
expect.equality(vim.api.nvim_get_current_win(), qf_win)
|
||||
expect.equality(#store.all(), 1)
|
||||
end)
|
||||
|
||||
it("does not use the open card when another quickfix list replaces ours", function()
|
||||
vim.cmd.Comment()
|
||||
local qf_win = vim.fn.getqflist({ winid = 0 }).winid
|
||||
vim.fn.setqflist({}, " ", { items = { { filename = "other.lua", lnum = 1, text = "other" } } })
|
||||
vim.api.nvim_set_current_win(qf_win)
|
||||
vim.cmd.CommentDelete()
|
||||
expect.equality(#store.all(), 2)
|
||||
expect.equality(helpers.notified()[#helpers.notified()], "Not a Scratch Comments list")
|
||||
end)
|
||||
|
||||
it("does not use an open card when the quickfix row is stale", function()
|
||||
local qf_win = vim.api.nvim_get_current_win()
|
||||
vim.cmd.Comment()
|
||||
local items = vim.fn.getqflist()
|
||||
items[2].user_data = {}
|
||||
vim.fn.setqflist({}, "r", { items = items })
|
||||
vim.api.nvim_set_current_win(qf_win)
|
||||
vim.api.nvim_win_set_cursor(qf_win, { 2, 0 })
|
||||
vim.cmd.CommentDelete()
|
||||
expect.equality(#store.all(), 2)
|
||||
expect.equality(helpers.notified()[#helpers.notified()], "This comment is gone")
|
||||
end)
|
||||
|
||||
it("does nothing after a card has closed", function()
|
||||
vim.cmd.Comment()
|
||||
vim.cmd.quit()
|
||||
vim.cmd.CommentDelete()
|
||||
expect.equality(#store.all(), 2)
|
||||
expect.equality(helpers.notified()[#helpers.notified()], "No comment card open")
|
||||
end)
|
||||
|
||||
it("protects unsaved changes when its quickfix row is selected", function()
|
||||
local qf_win = vim.api.nvim_get_current_win()
|
||||
vim.cmd.Comment()
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "unsaved" })
|
||||
vim.api.nvim_set_current_win(qf_win)
|
||||
vim.cmd.CommentDelete()
|
||||
expect.equality(#store.all(), 2)
|
||||
expect.equality(helpers.notified()[#helpers.notified()], "Save or discard the comment first")
|
||||
vim.api.nvim_set_current_win(
|
||||
assert(require("scratch_comments.ui.window").current()).comment_win
|
||||
)
|
||||
vim.cmd("q!")
|
||||
end)
|
||||
|
||||
it("protects unsaved changes", function()
|
||||
vim.cmd.Comment()
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "unsaved" })
|
||||
vim.cmd.CommentDelete()
|
||||
expect.equality(#store.all(), 2)
|
||||
expect.equality(helpers.notified()[#helpers.notified()], "Save or discard the comment first")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("saving an empty comment", function()
|
||||
it("deletes the edited comment and closes its card", function()
|
||||
vim.cmd.Comment()
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "" })
|
||||
vim.cmd.write()
|
||||
vim.wait(0)
|
||||
expect.equality(#store.all(), 1)
|
||||
expect.equality(views.anchored()[1].comment, "on one to three")
|
||||
expect.equality(#vim.fn.getqflist(), 1)
|
||||
end)
|
||||
|
||||
it("does not delete it when empty changes are discarded", function()
|
||||
vim.cmd.Comment()
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { "" })
|
||||
vim.cmd("q!")
|
||||
expect.equality(#store.all(), 2)
|
||||
expect.equality(helpers.notified()[#helpers.notified()], "No comment at cursor")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe(":CommentClear", function()
|
||||
it("removes every comment and its marks", function()
|
||||
local source = assert(views.anchored()[1].bufnr)
|
||||
vim.cmd("CommentClear")
|
||||
local namespace = vim.api.nvim_get_namespaces().scratch_comments
|
||||
expect.equality(#store.all(), 0)
|
||||
expect.equality(#vim.api.nvim_buf_get_extmarks(0, namespace, 0, -1, {}), 0)
|
||||
expect.equality(#vim.api.nvim_buf_get_extmarks(source, namespace, 0, -1, {}), 0)
|
||||
end)
|
||||
end)
|
||||
@@ -36,13 +36,23 @@ describe("a charwise selection", function()
|
||||
expect.equality(quick.end_col, 9)
|
||||
end)
|
||||
|
||||
it("opens an existing comment on the exact selection for editing", function()
|
||||
helpers.write("on quick")
|
||||
select("viw")
|
||||
vim.cmd("'<,'>Comment")
|
||||
expect.equality(helpers.text(), "on quick")
|
||||
helpers.write("edited quick")
|
||||
expect.equality(#views.anchored(), 1)
|
||||
expect.equality(views.anchored()[1].comment, "edited quick")
|
||||
end)
|
||||
|
||||
it("is separate from a comment on the whole line", function()
|
||||
helpers.write("on quick")
|
||||
vim.cmd("Comment")
|
||||
helpers.write("on the line")
|
||||
vim.cmd("1Comment")
|
||||
expect.equality(helpers.text(), "on the line")
|
||||
vim.cmd("quit")
|
||||
vim.cmd.quit()
|
||||
expect.equality(#views.anchored(), 2)
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -11,7 +11,9 @@ before_each(function()
|
||||
vim.cmd("1,3Comment")
|
||||
helpers.write("on one to three")
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
vim.cmd("CommentShow")
|
||||
vim.cmd.CommentList()
|
||||
vim.cmd("normal \r")
|
||||
vim.cmd("1,3Comment")
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
@@ -93,6 +95,76 @@ describe("the comment window", function()
|
||||
expect.equality(helpers.title(lines_win), " notes.md [lines 1–3] ")
|
||||
end)
|
||||
|
||||
it("resizes with its source window when quickfix closes", function()
|
||||
local source_win = require("scratch_comments.ui.window").current().source_win
|
||||
local dim = vim.iter(helpers.every_float()):find(function(win)
|
||||
return vim.w[win].scratch_comments_backdrop
|
||||
end)
|
||||
local before = vim.api.nvim_win_get_config(dim).height
|
||||
vim.cmd.cclose()
|
||||
vim.api.nvim_exec_autocmds("WinResized", {})
|
||||
expect.equality(vim.api.nvim_win_get_height(source_win) > before, true)
|
||||
expect.equality(
|
||||
vim.api.nvim_win_get_config(dim).height,
|
||||
vim.api.nvim_win_get_height(source_win)
|
||||
)
|
||||
end)
|
||||
|
||||
it("keeps a small card inside a very short source window", function()
|
||||
local record = assert(require("scratch_comments.ui.window").current())
|
||||
---@cast record { comment_win: integer, source_win: integer }
|
||||
local comment_buf = vim.api.nvim_win_get_buf(record.comment_win)
|
||||
vim.api.nvim_buf_set_lines(comment_buf, 0, -1, false, { "unsaved" })
|
||||
vim.api.nvim_set_current_win(record.source_win)
|
||||
vim.cmd("resize 3")
|
||||
vim.api.nvim_exec_autocmds("WinResized", {})
|
||||
local height = vim.api.nvim_win_get_height(record.source_win)
|
||||
local config = vim.api.nvim_win_get_config(record.comment_win)
|
||||
expect.equality(height < 6, true)
|
||||
expect.equality(config.row + config.height <= height, true)
|
||||
expect.equality(vim.bo[comment_buf].modified, true)
|
||||
expect.equality(helpers.text(comment_buf), "unsaved")
|
||||
end)
|
||||
|
||||
it("keeps a card in a narrow editing split", function()
|
||||
local record = assert(require("scratch_comments.ui.window").current())
|
||||
---@cast record { comment_win: integer, source_win: integer }
|
||||
vim.api.nvim_set_current_win(record.source_win)
|
||||
vim.cmd.vsplit()
|
||||
vim.api.nvim_set_current_win(record.source_win)
|
||||
vim.cmd("vertical resize 4")
|
||||
vim.api.nvim_exec_autocmds("WinResized", {})
|
||||
local config = vim.api.nvim_win_get_config(record.comment_win)
|
||||
expect.equality(vim.api.nvim_win_get_width(record.source_win) <= 5, true)
|
||||
expect.equality(config.width <= vim.api.nvim_win_get_width(record.source_win), true)
|
||||
end)
|
||||
|
||||
it("keeps unsaved edits when the source window closes", function()
|
||||
local record = assert(require("scratch_comments.ui.window").current())
|
||||
---@cast record { comment_win: integer, source_win: integer }
|
||||
local comment_buf = vim.api.nvim_win_get_buf(record.comment_win)
|
||||
vim.api.nvim_buf_set_lines(comment_buf, 0, -1, false, { "unsaved" })
|
||||
vim.api.nvim_set_current_win(record.source_win)
|
||||
vim.api.nvim_win_close(record.source_win, true)
|
||||
vim.wait(0)
|
||||
expect.equality(vim.api.nvim_win_is_valid(record.comment_win), true)
|
||||
expect.equality(helpers.text(comment_buf), "unsaved")
|
||||
expect.equality(vim.bo[comment_buf].modified, true)
|
||||
local parent = assert(vim.api.nvim_win_get_config(record.comment_win).win)
|
||||
expect.equality(vim.api.nvim_win_is_valid(parent), true)
|
||||
expect.no_equality(vim.bo[vim.api.nvim_win_get_buf(parent)].buftype, "quickfix")
|
||||
end)
|
||||
|
||||
it("closes a clean card if its source window was the last file pane", function()
|
||||
local record = assert(require("scratch_comments.ui.window").current())
|
||||
---@cast record { comment_win: integer, source_win: integer }
|
||||
vim.api.nvim_set_current_win(record.source_win)
|
||||
vim.api.nvim_win_close(record.source_win, true)
|
||||
vim.wait(0)
|
||||
expect.equality(vim.api.nvim_win_is_valid(record.comment_win), false)
|
||||
expect.equality(vim.bo.buftype, "quickfix")
|
||||
end)
|
||||
|
||||
it("closes on <Esc> in normal mode", function()
|
||||
vim.cmd([[execute "normal \<Esc>"]])
|
||||
expect.equality(#helpers.floats(), 0)
|
||||
@@ -117,7 +189,13 @@ describe("the comment window", function()
|
||||
end, helpers.every_float())
|
||||
expect.equality(#dim, 1)
|
||||
local config = vim.api.nvim_win_get_config(dim[1])
|
||||
expect.equality({ config.width, config.height }, { vim.o.columns, vim.o.lines })
|
||||
local source_win = require("scratch_comments.ui.window").current().source_win
|
||||
expect.equality(config.relative, "win")
|
||||
expect.equality(config.win, source_win)
|
||||
expect.equality(
|
||||
{ config.width, config.height },
|
||||
{ vim.api.nvim_win_get_width(source_win), vim.api.nvim_win_get_height(source_win) }
|
||||
)
|
||||
expect.equality(config.zindex < vim.api.nvim_win_get_config(0).zindex, true)
|
||||
expect.equality(vim.wo[dim[1]].winblend, 60)
|
||||
vim.cmd("quit")
|
||||
@@ -130,7 +208,8 @@ describe("the comment window", function()
|
||||
end
|
||||
vim.cmd("quit")
|
||||
local baseline = handlers()
|
||||
vim.cmd("CommentShow")
|
||||
vim.cmd.CommentList()
|
||||
vim.cmd.Comment()
|
||||
expect.equality(handlers() > baseline, true)
|
||||
vim.cmd("quit")
|
||||
expect.equality(handlers(), baseline)
|
||||
@@ -156,13 +235,29 @@ describe("the border", function()
|
||||
|
||||
it("is rounded when 'winborder' is not set", function()
|
||||
vim.o.winborder = ""
|
||||
vim.cmd("CommentShow")
|
||||
vim.cmd.CommentList()
|
||||
vim.cmd.Comment()
|
||||
expect.equality(vim.api.nvim_win_get_config(0).border[1], "╭")
|
||||
end)
|
||||
|
||||
it("follows 'winborder' when it is set", function()
|
||||
vim.o.winborder = "single"
|
||||
vim.cmd("CommentShow")
|
||||
vim.cmd.CommentList()
|
||||
vim.cmd.Comment()
|
||||
expect.equality(vim.api.nvim_win_get_config(0).border[1], "┌")
|
||||
end)
|
||||
|
||||
it("restores a custom border when a short source window grows again", function()
|
||||
vim.o.winborder = "single"
|
||||
vim.cmd.CommentList()
|
||||
vim.cmd.Comment()
|
||||
local record = assert(require("scratch_comments.ui.window").current())
|
||||
---@cast record { comment_win: integer, source_win: integer }
|
||||
vim.api.nvim_set_current_win(record.source_win)
|
||||
vim.cmd("resize 3")
|
||||
vim.api.nvim_exec_autocmds("WinResized", {})
|
||||
vim.cmd("resize 12")
|
||||
vim.api.nvim_exec_autocmds("WinResized", {})
|
||||
expect.equality(vim.api.nvim_win_get_config(record.comment_win).border[1], "┌")
|
||||
end)
|
||||
end)
|
||||
@@ -34,6 +34,11 @@ describe("title", function()
|
||||
end)
|
||||
|
||||
describe("fit", function()
|
||||
it("returns no title when no room is available", function()
|
||||
expect.equality(location.fit("app.lua", 0), "")
|
||||
expect.equality(location.fit("app.lua", -1), "")
|
||||
end)
|
||||
|
||||
it("leaves text that fits alone", function()
|
||||
expect.equality(location.fit("app.lua [line 3]", 16), "app.lua [line 3]")
|
||||
end)
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
local paths = require("scratch_comments.paths")
|
||||
local expect = MiniTest.expect
|
||||
|
||||
describe("is_uri", function()
|
||||
it("recognizes provider-neutral URI buffer names", function()
|
||||
expect.equality(paths.is_uri("codediff:////repo///HEAD/a.lua"), true)
|
||||
expect.equality(paths.is_uri("review+git://repo/file.lua"), true)
|
||||
expect.equality(paths.is_uri("/repo/file.lua"), false)
|
||||
expect.equality(paths.is_uri("file:with-colon.lua"), false)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("git_root", function()
|
||||
it("finds the repository a file is in", function()
|
||||
local root = vim.fn.getcwd()
|
||||
|
||||
Reference in new issue
Block a user