Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ has a `<Plug>(changeset-…)` map named after its words, and these default keys:
| `f` | `filter` | Filter the tree as you type |
| `F` | `filter_kinds` | Hide symbol kinds, remembered for this branch, repository or everywhere |
| `y` | `yank` | Copy the row's `path:line`, or a ranged comment's `path:first-last` |
| `x` | `toggle_viewed` | Mark the file viewed, or not, until its diff changes |
| `X` | `toggle_viewed_all` | Mark every file viewed, or none |
| `d` | `delete_comment` | Delete the review comment on a Comments row |
| `R` | `refresh` | Rebuild the tree |
| `?` | `help` | List the sidebar's keys |
Expand Down Expand Up @@ -219,6 +221,8 @@ require("changeset").setup({
prev_section = "[[",
refresh = "R",
yank = "y",
toggle_viewed = "x",
toggle_viewed_all = "X",
delete_comment = "d",
help = "?",
filter_kinds = "F",
Expand Down
69 changes: 66 additions & 3 deletions doc/agents/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ restarts. With mini.icons:
- Symbol rows: `MiniIcons.get("lsp", kind)` — the exact call that config's outline picker
makes, so a method is the same glyph in the same hue in both the outline picker and the
sidebar.
- File rows: `MiniIcons.get("file", path)`.
- File rows: `MiniIcons.get("file", path)`, or the viewed check (§ A viewed file
recedes).
- Section headers: `MiniIcons.get("directory", …)` with `src`, `tests`, `docs`,
`.config` and `build`, so each header wears the icon its kind of directory already has.
- Orphan-hunk groups: the `lsp`/`Text` icon, dimmed. Not a bespoke glyph.
Expand All @@ -167,6 +168,49 @@ restarts. With mini.icons:

A future icon-set change propagates everywhere at once. That is the point.

### A viewed file recedes

A file marked viewed (`x`) is one the user has finished with, so it steps back in four ways,
in every section it shows in. Three of them (wash, check, label) are quiet signals that add
no new hue, so the status rail stays the one bold element.

- **A wash.** The file row and every line under it take `ChangesetViewed`, a background 8%
of the way from the window's to `Comment`'s grey. Every row state tints toward
`Statement`'s accent (selected 20%, here 12%, picked 6%), so a neutral wash never reads as
one of them. It is clearly fainter than the selection, about as quiet as picked, and draws
beneath all three, which still show over it. It draws beneath the pointer's
`ChangesetHover` too, which would otherwise vanish across a viewed file's rows.
- **A check in the icon's place.** `✔` in `ChangesetViewedIcon`, linked to `Comment`, mirroring
the `ChangesetSelected`/`ChangesetSelectedIcon` pair. The checks fill the icon column, so
finished files read down one edge, as a checklist. It is one cell, so the width budget is
unchanged. The config already uses `✔` as its "selected" marker, and no icon plugin has a
"viewed" category to ask for. `Comment`'s grey is the tree's "not the point" colour;
`DiagnosticOk` green already means "review comment".
- **A label.** `viewed`, right-aligned just left of the stat, in `ChangesetMeta`: the "not
content" level of the three levels of emphasis. It counts in the width budget, so the
name trims first. It says `viewed`, not "(read)", so the feature keeps one name: the
`toggle_viewed` option, `viewed.json`, GitHub's "Viewed".
- **A fold.** Marking folds every copy of the file, to give the rest the room; unmarking
unfolds them. A file whose mark holds the first time the session draws it at that digest
starts folded, which covers a restart and a mark another Neovim wrote. After that the
user's fold wins: one opened with `l` stays open until it is marked again. A mark that
stops holding leaves the fold alone, since the user may already be reading the change.

Marking then moves the cursor to the next file row below that is not viewed, skipping other
copies of the marked file, and previews it as a cursor move does: reviewing is a walk down
the files. With none below, the cursor stays on the marked row. Unmarking leaves it put.

`X` marks every file viewed, or clears every mark once all are, so one key both finishes a
review and starts it over.

The key is `x`, which the kind menu already uses to hide or show the kind under the cursor,
so `x` means "toggle this row" in both places.

Placements rejected for the check: the left margin, where it would butt against the rail;
after the name, where it would move with the name's length and could be matched by the
filter; the right gutter, which the row-state glyphs own; and dimming the name, since
`Comment` on a name already means "context".

### Kind labels are dropped; the icon carries kind

The config's outline picker right-aligns a kind label (`Method`, `Class`). A sidebar is
Expand Down Expand Up @@ -1219,7 +1263,9 @@ open as it does.
gitsigns keeps one view per window and drops it when the buffer leaves the window, so
changeset opens one wherever a file comes to be shown: a window taking a buffer, a buffer or
window entered, which covers a split of a window already showing it, and a preview, whose
buffer swap fires no autocommand and so opens its own. A file gitsigns has not read yet
buffer swap fires no autocommand and so opens its own. Nor does that swap close the view
on the file it swaps out, so changeset closes it then, or gitsigns would hand it back
uncovered when the file returns. A file gitsigns has not read yet
gets its view on its `GitSignsUpdate`, once the base text is in. The same event, and
entering the window, reopen a view whose base gitsigns has since replaced, as the
watcher's own move of the base does. A move that leaves the counts alone
Expand Down Expand Up @@ -1313,6 +1359,11 @@ there with gitsigns' signs drawn beside it, and they stay closed until the diff
on, by the sidebar or the toggle, each the user asking for the diff again; nothing writes it
down. Turning every file off for it would make one stray toggle cost the session's diff.

A file marked viewed has no diff while its mark holds: the user has read it, so its buffer
goes back to how gitsigns alone draws it. The diff returns when the file's diff changes,
since there is something new to read, or when the mark is cleared. Viewed is its own
condition, apart from the reach and from a view closed by hand.

## What it remembers

The tree is built the first time something asks for it — `:Changeset`, `:Changeset
Expand Down Expand Up @@ -1358,9 +1409,21 @@ is never parsed again and its base is never read again. The file name carries a
format number, bumped whenever what an entry holds or how it is derived changes, because an
older entry's stamp would otherwise still match. Folds — a section's as well as a file's — are remembered per
repository for as long as Neovim is running, so reopening looks like you left it; a restart
starts expanded, except Generated, which starts folded. Per repository because a row is
starts expanded, except Generated, which starts folded. Beside the folds, each viewed file
records the digest it was last folded at, so a viewed file folds once per diff and the
user's unfold sticks. Per repository because a row is
identified by a repo-relative path, which two checkouts can easily both have.

Files marked viewed are kept in `stdpath("state")/changeset/viewed.json`, per repository
root and path, so every copy of a file (its path section, Tests, Docs) shows the mark. Each
mark holds the digest of the diff it was viewed at, not the File's identity: identity is
per tree and per session, while a digest names the exact diff, so a mark stops showing once
the diff changes and shows again if the change is reverted. A patch with no text, as a
binary file's, digests its status and old path, so editing it in place keeps its mark. The
digest is one of the fields `keep_identity` compares, so rewriting a line at the same counts
gives the file a new identity and redraws its row. An unreadable record is never written
over, since its marks can't be derived again.

## Hidden kinds

Which symbol kinds are hidden is not a setting: it is a choice made in the menu and
Expand Down
67 changes: 66 additions & 1 deletion doc/changeset.nvim.txt
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,53 @@ keymaps.yank *changeset-option-keymaps.yank*
provider. A Comments row on a range copies `path:first-last`. A row
with no line copies the path alone.

x *changeset-sidebar-x*
keymaps.toggle_viewed *changeset-option-keymaps.toggle_viewed*
Mark the file viewed, or clear its mark. It acts on the file the row
belongs to: its file row, or a symbol's, a change's or an "Other
changes" row under it. Nothing on a section or Comments row. If the
mark can't be written, changeset reports an error and changes nothing.

A viewed file changes in every section it shows in:
- Its file row and every line under it take a faint |ChangesetViewed|
wash, under the |ChangesetSelected|, |ChangesetHere|,
|ChangesetPicked| and |ChangesetHover| tints.
- A |ChangesetViewedIcon| `✔` takes its icon's place.
- The word `viewed` sits left of its stat, in |ChangesetMeta|. A long
name is trimmed to make room.
- The file folds.
Its unified diff closes too (|changeset-unified-diff|).

After marking, the cursor moves to the next file row below that isn't
viewed, skipping other copies of the file you marked, and previews it.
With none below, it stays on the marked file. Clearing a mark unfolds
the file and leaves the cursor where it is.

A file whose mark holds starts folded the first time the sidebar draws
it at that diff, as after a restart. After that your own fold wins: a
viewed file you open with `l` stays open until you mark it again.

A mark holds only while the file's diff is the one you viewed. It
stops showing once that diff changes, and shows again if the change
is reverted or the base switched back. A mark that stops holding
leaves the fold as it is. `x` on a file whose mark no longer holds
marks its new diff. A file whose patch has no text, such as a binary
file edited in place, keeps its mark. Marks another Neovim writes show
once the tree is built again for another branch or base, or after
your next `x`.

X *changeset-sidebar-X*
keymaps.toggle_viewed_all *changeset-option-keymaps.toggle_viewed_all*
Mark every file the tree lists viewed, or, when every one already is,
clear every mark. It covers each file whatever a filter, a hidden kind
or a folded section hides, deleted files too. A viewed file looks as
|changeset-sidebar-x| describes. If the marks can't be written,
changeset reports an error and changes nothing.

After marking, every file folds and the cursor moves to its file's own
row in the section it was in. Clearing unfolds every file and leaves
the cursor where it is.

d *changeset-sidebar-d*
keymaps.delete_comment *changeset-option-keymaps.delete_comment*
On a Comments row, ask, then delete its review comment. Nothing on any
Expand Down Expand Up @@ -1137,6 +1184,13 @@ anything else gitsigns offers, closes changeset's views of that file in every
window as you leave it, and keeps them closed until the diff next turns on, by
the sidebar opening or |:Changeset-diff|. Other files keep theirs.

changeset keeps the diff off a file marked viewed (|changeset-sidebar-x|)
while its mark holds. Marking it closes its views in every window, and its
buffer goes back to how gitsigns alone draws it, signs included. Clearing the
mark opens them again where the diff reaches the file. So does editing the
file, once the sidebar has read the new diff. Undoing that edit, so the diff
is the one you viewed, closes them again.

A file new since that base, untracked or not, shows every line added. While
the diff is on, gitsigns attaches to untracked files, as its
`attach_to_untracked` option would have it; turning the diff off puts that
Expand Down Expand Up @@ -1277,6 +1331,13 @@ ChangesetMatch *ChangesetMatch*
ChangesetHidden *ChangesetHidden*
A hidden kind in the kind menu. `Comment`'s colour, struck through.

ChangesetViewed *ChangesetViewed*
The background of a viewed file's rows (|changeset-sidebar-x|).
`Normal`'s background tinted faintly toward `Comment`'s colour.

ChangesetViewedIcon *ChangesetViewedIcon*
The `✔` in place of a viewed file's icon. Links to `Comment`.

ChangesetSelected *ChangesetSelected*
The selected row while the sidebar has focus. `Normal`'s background
tinted toward `Statement`.
Expand Down Expand Up @@ -1478,14 +1539,18 @@ nothing, starts no language server and makes no network request.
once.
- Hidden symbol kinds: `stdpath("state")/changeset/filters.json`.
- Bases set with |:Changeset-base|: `stdpath("state")/changeset/bases.json`.
- Files marked viewed with |changeset-sidebar-x|:
`stdpath("state")/changeset/viewed.json`. An unreadable one is never
written over.
- Swap files: a file changeset reads or previews gets none until you enter
its buffer, so another Neovim opening it shows no |E325| warning.
- Review comments, and each branch's last 10 submitted batches:
`stdpath("state")/changeset/comments.json`.
- Sessions: |:mksession| restores the sidebar, in whichever tabpage it stood,
when 'sessionoptions' contains `blank`, as it does by default, and its
cursor row and scroll too when it also contains `globals`.
- Kept in memory until Neovim exits: folds, the agent last submitted to, how
- Kept in memory until Neovim exits: folds, which diff each viewed file
was last folded at, the agent last submitted to, how
far the unified diff reaches, the files you entered
(|changeset-entered-files|), and the files whose view you closed.

Expand Down
78 changes: 78 additions & 0 deletions lua/changeset/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ local icons = require("changeset.icons")
local review_comment = require("changeset.review_comment")
local reviewing = require("changeset.reviewing")
local sidebar_state = require("changeset.sidebar_state")
local unified_diff = require("changeset.unified_diff")
local view = require("changeset.view")
local viewed = require("changeset.viewed")
local window = require("changeset.window")

-- input() reads a line, so it can never hand one back: free to mean "cancelled".
Expand Down Expand Up @@ -317,6 +319,28 @@ local function prompt_filter(state, redraw)
redraw()
end

---The row to put the cursor on after marking `path` viewed from line `lnum` of `shown`: the next file row below that
---is not viewed and is not another copy of `path`, else `path`'s own row in the section `lnum` is in.
---@param shown changeset.Row[] The row on each line.
---@param lnum integer
---@param path string
---@param is_viewed fun(row: changeset.Row): boolean
---@return changeset.Row?
function M._after_viewing(shown, lnum, path, is_viewed)
for i = lnum + 1, #shown do
local row = shown[i]
if row.kind == "file" and row.path ~= path and not is_viewed(row) then
return row
end
end
for i = lnum, 1, -1 do
local row = assert(shown[i], "changeset: a line above the cursor without a row")
if row.kind == "file" and row.path == path then
return row
end
end
end

---Bind `keys` on the sidebar's buffer. `?` lists exactly these.
---@param buf integer
---@param keys changeset.Options.Keymaps
Expand Down Expand Up @@ -344,6 +368,39 @@ function M.set_keymaps(buf, keys, hooks)
end, desc)
end

---Show `marks`, just written for `paths`, or report they could not be: unview `paths` when the mark of `path`, the
---cursor's file, no longer holds, else move the cursor past it.
---@param state changeset.SidebarState
---@param marks changeset.ViewedMarks?
---@param paths string[]
---@param path string?
local function show_marks(state, marks, paths, path)
if not marks then
return vim.notify("Changeset: could not write " .. viewed.path(), vim.log.levels.ERROR)
end
state.marks = marks
unified_diff.refresh_viewed()
local file = path and vim.iter(state.tree.files):find(function(f)
return f.path == path
end)
if not (file and viewed.holds(marks, file.path, file.digest)) then
for _, p in ipairs(paths) do
state.view:unview(state.rows, p)
end
return redraw()
end
local shown, lnum = state.view:visible(), assert(window.cursor(), "changeset: sidebar is closed")
local target = M._after_viewing(shown, lnum, file.path, function(r)
return viewed.holds(marks, r.path, r.digest)
end)
redraw()
for at, r in ipairs(state.view:visible()) do
if target and r.id == target.id then
return move(at)
end
end
end

map(keys.jump, function()
jump("reuse", hooks)
end, "Go to this change")
Expand Down Expand Up @@ -437,6 +494,27 @@ function M.set_keymaps(buf, keys, hooks)
or row.path
Paths.copy(text, "relative path:line")
end, "Yank path:line")
map(keys.toggle_viewed, function(state)
local row = draw.row_at_cursor()
if not row or row.kind == "section" or row.kind == "comment" then
return
end
local file = vim.iter(state.tree.files):find(function(f)
return f.path == row.path
end)
if not file then
return
end
show_marks(state, viewed.toggle(viewed.path(), state.tree.root, file.path, file.digest), { file.path }, file.path)
end, "Mark the file viewed, or not")
map(keys.toggle_viewed_all, function(state)
local row = draw.row_at_cursor()
local files = state.tree.files
local paths = vim.tbl_map(function(f)
return f.path
end, files)
show_marks(state, viewed.toggle_all(viewed.path(), state.tree.root, files), paths, row and row.path)
end, "Mark every file viewed, or none")
map(keys.help, function()
help.show(buf, own)
end, "Show these keymaps")
Expand Down
2 changes: 1 addition & 1 deletion lua/changeset/build.lua
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ local function file_answer(root, path, answer, stamp)
end

local HUNK_FIELDS = { "lnum", "count", "added", "removed", "old_lnum" }
local FILE_FIELDS = { "path", "oldpath", "status", "section", "added", "removed" }
local FILE_FIELDS = { "path", "oldpath", "status", "section", "added", "removed", "digest" }

---Whether `a` and `b` hold the same diff of the same file.
---@param a changeset.File
Expand Down
6 changes: 6 additions & 0 deletions lua/changeset/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ local M = {}
---@field prev_section? string|false Previous section. Default `[[`.
---@field refresh? string|false Rebuild the tree. Default `R`.
---@field yank? string|false Yank path:line. Default `y`.
---@field toggle_viewed? string|false Mark the file viewed, or not. Default `x`.
---@field toggle_viewed_all? string|false Mark every file viewed, or none. Default `X`.
---@field delete_comment? string|false Delete the review comment a Comments row lists. Default `d`.
---@field help? string|false Show these keymaps. Default `?`.
---@field filter_kinds? string|false Filter by symbol kind. Default `F`.
Expand Down Expand Up @@ -67,6 +69,8 @@ local M = {}
---@field prev_section string|false
---@field refresh string|false
---@field yank string|false
---@field toggle_viewed string|false
---@field toggle_viewed_all string|false
---@field delete_comment string|false
---@field help string|false
---@field filter_kinds string|false
Expand Down Expand Up @@ -104,6 +108,8 @@ local DEFAULTS = {
prev_section = "[[",
refresh = "R",
yank = "y",
toggle_viewed = "x",
toggle_viewed_all = "X",
delete_comment = "d",
help = "?",
filter_kinds = "F",
Expand Down
Loading
Loading