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
17 changes: 12 additions & 5 deletions lua/diffview/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ local lazy = require("diffview.lazy")

local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
local Diff1Inline = lazy.access("diffview.scene.layouts.diff_1_inline", "Diff1Inline") ---@type Diff1Inline|LazyModule
local Diff1InlinePinned =
lazy.access("diffview.scene.layouts.diff_1_inline_pinned", "Diff1InlinePinned") ---@type Diff1InlinePinned|LazyModule
local Diff1Pinned = lazy.access("diffview.scene.layouts.diff_1_pinned", "Diff1Pinned") ---@type Diff1Pinned|LazyModule
local Diff2 = lazy.access("diffview.scene.layouts.diff_2", "Diff2") ---@type Diff2|LazyModule
local Diff2Hor = lazy.access("diffview.scene.layouts.diff_2_hor", "Diff2Hor") ---@type Diff2Hor|LazyModule
local Diff2HorPinned = lazy.access("diffview.scene.layouts.diff_2_hor_pinned", "Diff2HorPinned") ---@type Diff2HorPinned|LazyModule
Expand Down Expand Up @@ -34,11 +37,11 @@ function M.diffview_callback(cb_name)
end

-- Layout aliases used across multiple view kinds and cycle_layouts. The
-- pinned `Diff2` variants (`diff2_*_pinned`) intentionally aren't listed
-- here: they're internal to file-history `pin_local` mode and are
-- selected by the view based on whether `--pin-local` is active, not by
-- direct user configuration. The `LayoutName` alias below still includes
-- them so `name_to_layout` can resolve them internally.
-- pinned variants (`diff1_*_pinned`, `diff2_*_pinned`) intentionally
-- aren't listed here: they're internal to file-history `pin_local` mode
-- and are selected by the view based on whether `--pin-local` is active,
-- not by direct user configuration. The `LayoutName` alias below still
-- includes them so `name_to_layout` can resolve them internally.
---@alias DiffviewStandardLayout "diff1_plain"|"diff1_inline"|"diff2_horizontal"|"diff2_vertical"
---@alias DiffviewMergeLayout "diff1_plain"|"diff3_horizontal"|"diff3_vertical"|"diff3_mixed"|"diff4_mixed"
---@alias DiffviewInferredLayout -1
Expand Down Expand Up @@ -879,7 +882,9 @@ function M.get_log_options(single_file, t, vcs)
end

---@alias LayoutName "diff1_plain"
--- | "diff1_plain_pinned"
--- | "diff1_inline"
--- | "diff1_inline_pinned"
--- | "diff2_horizontal"
--- | "diff2_horizontal_pinned"
--- | "diff2_vertical"
Expand All @@ -891,7 +896,9 @@ end

local layout_map = {
diff1_plain = Diff1,
diff1_plain_pinned = Diff1Pinned,
diff1_inline = Diff1Inline,
diff1_inline_pinned = Diff1InlinePinned,
diff2_horizontal = Diff2Hor,
diff2_horizontal_pinned = Diff2HorPinned,
diff2_vertical = Diff2Ver,
Expand Down
6 changes: 4 additions & 2 deletions lua/diffview/scene/layouts/diff_1_inline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -443,10 +443,12 @@ end
---@override
---Diff1Inline owns `a_file` even though it isn't attached to a window, so
---expose it through `owned_files()` so `FileEntry:destroy()` can tear it
---down alongside the windowed files.
---down alongside the windowed files. Defer to `Layout:owned_files` for the
---windowed slots so `shared_symbols` (e.g. `Diff1InlinePinned`'s borrowed
---b-side) is honoured in subclasses.
---@return vcs.File[]
function Diff1Inline:owned_files()
local out = Layout.files(self)
local out = Layout.owned_files(self)
if self.a_file and not vim.tbl_contains(out, self.a_file) then
out[#out + 1] = self.a_file
end
Expand Down
28 changes: 28 additions & 0 deletions lua/diffview/scene/layouts/diff_1_inline_pinned.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
local Diff1Inline = require("diffview.scene.layouts.diff_1_inline").Diff1Inline
local Diff1Pinned = require("diffview.scene.layouts.diff_1_pinned").Diff1Pinned
local oop = require("diffview.oop")

local M = {}

---@class Diff1InlinePinned : Diff1Inline
---Inline variant of `Diff1Pinned`: renders the diff inline against the
---view's shared working-tree b-file. The b-side `vcs.File` is owned by the
---FileHistoryView (its pin_local cache); `shared_symbols = { "b" }` keeps
---`Layout:owned_files()` from returning it, so `FileEntry:destroy` won't
---tear it down. The view destroys the cached b-file once in `close()`.
---Inherits the rest of `Diff1Inline`'s rendering and `Diff1Pinned`'s
---`detach_files_for_swap` semantics.
local Diff1InlinePinned = oop.create_class("Diff1InlinePinned", Diff1Inline)

Diff1InlinePinned.name = "diff1_inline_pinned"
Diff1InlinePinned.shared_symbols = { "b" }

---@param opt Diff1Inline.init.Opt
function Diff1InlinePinned:init(opt)
self:super(opt)
end

Diff1InlinePinned.detach_files_for_swap = Diff1Pinned.detach_files_for_swap

M.Diff1InlinePinned = Diff1InlinePinned
return M
46 changes: 46 additions & 0 deletions lua/diffview/scene/layouts/diff_1_pinned.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
local Diff1 = require("diffview.scene.layouts.diff_1").Diff1
local oop = require("diffview.oop")

local M = {}

---@class Diff1Pinned : Diff1
---A single-window Diff1 whose b-window pins to a working-tree LOCAL buffer
---across log navigation. The b-side `vcs.File` is owned by the
---FileHistoryView (its pin_local cache), not by individual FileEntries.
---Listing "b" in `shared_symbols` keeps `Layout:owned_files()` from
---returning it, so `FileEntry:destroy` and `FileEntry:set_active` walk past
---the borrowed file. The view tears it down in `close()` once.
local Diff1Pinned = oop.create_class("Diff1Pinned", Diff1)

Diff1Pinned.name = "diff1_plain_pinned"
Diff1Pinned.shared_symbols = { "b" }

---@param opt Diff1.init.Opt
function Diff1Pinned:init(opt)
self:super(opt)
end

-- Mirror `Diff2HorPinned:detach_files_for_swap`: skip detaching window b
-- when the next entry's b is the same `vcs.File` instance so the pinned
-- LOCAL buffer's diffview keymaps and edits survive the swap. In
-- multi-file pinning a row change can swap b to a different working-tree
-- File (each path has its own view-owned File); detach the old one in
-- that case so its buffer doesn't keep stale diffview state attached.
-- The inherited `detach_files()` still runs on tab-leave / view-close.
---@override
---@param next_entry? FileEntry
function Diff1Pinned:detach_files_for_swap(next_entry)
-- Without a next entry we have no comparison; preserve the old
-- "skip detach" behaviour for callers that haven't migrated to the
-- new signature.
if self.b and next_entry then
local next_layout = next_entry.layout --[[@as Diff1Pinned ]]
local next_b = next_layout and next_layout.b and next_layout.b.file
if next_b ~= self.b.file then
self.b:detach_file()
end
end
Comment thread
dlyongemallo marked this conversation as resolved.
end

M.Diff1Pinned = Diff1Pinned
return M
6 changes: 3 additions & 3 deletions lua/diffview/scene/layouts/diff_2_hor_pinned.lua
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ function Diff2HorPinned:detach_files_for_swap(next_entry)
if self.a then
self.a:detach_file()
end
-- Without a next entry we have no comparison; preserve the old
-- "skip detach" behaviour for callers that haven't migrated to the
-- new signature.
if self.b and next_entry then
-- Without a next entry we have no comparison; preserve the old
-- "skip detach" behaviour for callers that haven't migrated to the
-- new signature.
local next_layout = next_entry.layout --[[@as Diff2HorPinned ]]
local next_b = next_layout and next_layout.b and next_layout.b.file
if next_b ~= self.b.file then
Expand Down
4 changes: 2 additions & 2 deletions lua/diffview/scene/layouts/diff_2_ver_pinned.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ function Diff2VerPinned:detach_files_for_swap(next_entry)
if self.a then
self.a:detach_file()
end
-- See `Diff2HorPinned:detach_files_for_swap` for the no-next-entry
-- carve-out.
if self.b and next_entry then
-- See `Diff2HorPinned:detach_files_for_swap` for the no-next-entry
-- carve-out.
local next_layout = next_entry.layout --[[@as Diff2VerPinned ]]
local next_b = next_layout and next_layout.b and next_layout.b.file
if next_b ~= self.b.file then
Expand Down
86 changes: 52 additions & 34 deletions lua/diffview/scene/views/file_history/file_history_view.lua
Original file line number Diff line number Diff line change
Expand Up @@ -532,24 +532,35 @@ function FileHistoryView:should_show_panel()
return config.get_config().file_history_panel.show
end

-- Map a non-pinned Diff2 layout name to its pinned counterpart. Pinned
-- layouts share window orientation with their unpinned siblings; we just
-- re-route the layout class so the b-window keeps its file across entry
-- swaps. Names that have no pinned variant fall through unchanged.
-- Map a non-pinned layout name to its pinned counterpart. Pinned variants
-- share window orientation with their unpinned siblings; we re-route the
-- layout class so the b-window keeps its file across entry swaps via
-- `shared_symbols = { "b" }`. Diff1/Diff1Inline have pinned variants too
-- (their b-side is also bound to the view-owned working-tree file in
-- pin_local mode); names without a pinned sibling fall through unchanged.
local pinned_variant = {
diff1_plain = "diff1_plain_pinned",
diff1_inline = "diff1_inline_pinned",
diff2_horizontal = "diff2_horizontal_pinned",
diff2_vertical = "diff2_vertical_pinned",
}

-- Inverse of `pinned_variant`. A pinned class is only valid when adapters
-- inject `revs.a = COMMIT` (which only happens under `pin_local`); applied
-- to a parent-vs-commit history the pinned `should_null` mis-classifies
-- status "A"/"?" and the adapter then fails to `show <rev>:<missing>`. The
-- user-config path is already gated by `config`'s `standard_layouts`
-- validation (pinned names aren't in the schema's allow-list), but we
-- still fold pinned → unpinned here as belt-and-suspenders for any other
-- caller (tests, future code) that reaches `get_default_layout` with a
-- pinned name and `pin_local` off.
-- Inverse of `pinned_variant`. Pinned classes only make sense in
-- `pin_local` mode: they all declare `shared_symbols = { "b" }` and expect
-- the FileHistoryView to own the b-side `vcs.File` via its pin_local cache,
-- so outside `pin_local` there's no shared owner and the b-side would
-- never be torn down. The Diff2 pinned variants are additionally unsafe
-- there because they override `should_null` with parent-vs-commit semantics
-- that assume `revs.a = COMMIT` (only injected under `pin_local`); applied
-- to a parent-vs-commit history they mis-classify status "A"/"?" and the
-- adapter then fails to `show <rev>:<missing>` (the Diff1 pinned variants
-- inherit `Diff1.should_null` unchanged, so they don't have that specific
-- bug, but the shared-b ownership mismatch still applies). The user-config
-- path is already gated by `config`'s `standard_layouts` validation
-- (pinned names aren't in the schema's allow-list), but we still fold
-- pinned → unpinned here as belt-and-suspenders for any other caller
-- (tests, future code) that reaches `get_default_layout` with a pinned
-- name and `pin_local` off.
local unpinned_variant = {}
for unpinned, pinned in pairs(pinned_variant) do
unpinned_variant[pinned] = unpinned
Expand All @@ -564,22 +575,24 @@ function FileHistoryView:get_default_layout()
name = FileHistoryView.get_default_diff2().name
end

local resolved
if self.pin_local then
-- pin_local needs a pinned-Diff2 layout: only those declare
-- `shared_symbols = { "b" }`, which is what keeps `FileEntry:destroy`
-- from tearing down the view-owned working-tree file on every refresh.
-- If the user's configured layout doesn't have a pinned variant
-- (e.g. `diff1_inline`), fall back to the default Diff2 so the
-- shared-b-side mechanism actually engages.
if not pinned_variant[name] then
name = FileHistoryView.get_default_diff2().name
end
name = pinned_variant[name]
-- Upgrade standard layout names to their pinned siblings so the
-- shared-b mechanism engages: pinned variants declare
-- `shared_symbols = { "b" }`, which keeps `FileEntry:destroy` from
-- tearing down the view-owned working-tree file on every refresh.
-- All standard layouts (`diff1_*`, `diff2_*`) have pinned siblings,
-- so the `pinned_variant` lookup normally hits. If a non-standard
-- name ever reaches here (e.g. via a future non-config caller that
-- bypasses the `standard_layouts` allow-list), fall back to the
-- default pinned Diff2 -- matching `resolve_pinned_layout` -- so the
-- shared-b contract still holds.
resolved = pinned_variant[name] or pinned_variant[FileHistoryView.get_default_diff2().name]
else
name = unpinned_variant[name] or name
resolved = unpinned_variant[name] or name
end

return config.name_to_layout(name --[[@as string ]])
return config.name_to_layout(resolved --[[@as string ]])
end

---Inverse of `resolve_pinned_layout`: map a pinned class to its unpinned
Expand All @@ -599,17 +612,19 @@ function FileHistoryView:unpinned_layout(layout_class)
end

---Map an arbitrary layout class to the right one for this view's pin_local
---state. Used by `cycle_layout` / `set_layout` so neither action can drop a
---pin_local FileHistoryView into an unpinned `Diff2` (which would cause
---state. Used by `cycle_layout` / `set_layout` so neither action drops a
---pin_local FileHistoryView into an unpinned variant (which would cause
---`FileEntry:destroy` to tear down the view-owned working-tree file once
---per entry, and would untie the b-window from its shared LOCAL buffer).
---When `pin_local` is off, returns the input unchanged. When on:
--- - already a pinned variant: returns it unchanged (preserves the user's
--- chosen orientation).
--- - has a pinned sibling (e.g. `diff2_horizontal`): returns the pinned
--- sibling.
--- - no pinned variant (e.g. `diff1_inline`): returns the configured
--- default Diff2's pinned form, so the shared-b mechanism still engages.
--- - has a pinned sibling (e.g. `diff2_horizontal`, `diff1_inline`):
--- returns the pinned sibling.
--- - no pinned variant (e.g. `diff3_*`/`diff4_*` reaching us via a
--- user-supplied `view.cycle_layouts.default` entry or a direct
--- `actions.set_layout` call): falls back to the default Diff2's pinned
--- form so the shared-b contract still holds.
---@param layout_class Layout (class)
---@return Layout (class)
function FileHistoryView:resolve_pinned_layout(layout_class)
Expand All @@ -623,11 +638,14 @@ function FileHistoryView:resolve_pinned_layout(layout_class)
return layout_class
end

if not pinned_variant[name] then
name = FileHistoryView.get_default_diff2().name
local sibling = pinned_variant[name]
if sibling then
return config.name_to_layout(sibling --[[@as string ]])
end

return config.name_to_layout(pinned_variant[name] --[[@as string ]])
return config.name_to_layout(
pinned_variant[FileHistoryView.get_default_diff2().name] --[[@as string ]]
)
end

M.FileHistoryView = FileHistoryView
Expand Down
Loading
Loading