From bc74a998dc12f828024daead12bd8054505fe7a4 Mon Sep 17 00:00:00 2001 From: Tummetott <19708025+Tummetott@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:40:41 +0100 Subject: [PATCH] feat(view): expose revision and file context to diff buffer hooks The `diff_buf_read` and `diff_buf_win_enter` hooks only reported the window's layout position, so consumers such as statusline and winbar plugins had to walk Diffview internals to learn which revision a buffer shows. Extend the hook context with the buffer's revision (`type`, `commit`, `stage`, `tracks_head`), its repo-relative `path`, the repository `toplevel` and the `adapter` name. The `DiffviewDiffBufRead` and `DiffviewDiffBufWinEnter` User autocmds carry the same fields in `data`, together with `bufnr` and `winid`. --- doc/diffview.txt | 40 +++++++++++++++---- lua/diffview/config.lua | 10 +++++ lua/diffview/init.lua | 16 ++++++-- lua/diffview/scene/window.lua | 27 +++++++++++-- lua/diffview/tests/functional/window_spec.lua | 31 ++++++++++++++ 5 files changed, 109 insertions(+), 15 deletions(-) diff --git a/doc/diffview.txt b/doc/diffview.txt index e462e534..3bbb856f 100644 --- a/doc/diffview.txt +++ b/doc/diffview.txt @@ -1789,13 +1789,38 @@ hooks *diffview-config-hooks* Callback Parameters: ~ {bufnr} (`integer`) The buffer number of the new buffer. - {ctx} (`table`) + {ctx} (`table`) *diffview-diff-buf-ctx* • {symbol} (string) A symbol that identifies the window's position in the layout. These symbols correspond with the figures under |diffview-config-view.x.layout|. • {layout_name} (string) The name of the current layout. + • {rev} (table) + The revision the buffer shows: + • {type} (string) + `"LOCAL"` (the working tree), `"COMMIT"`, + `"STAGE"` (an index stage), or `"CUSTOM"` + (content outside any VCS). + • {commit} (string|nil) + The full object name, for `"COMMIT"`. + • {stage} (integer|nil) + The stage number, for `"STAGE"`: 0 for the + index, 1 for the common ancestor, 2 for ours + and 3 for theirs. + • {tracks_head} (boolean) + Whether the revision was resolved from HEAD + and follows it when HEAD moves. + • {path} (string) + The file path, relative to {toplevel}. + • {toplevel} (string) + The root of the repository. Outside a repository, + the directory {path} is relative to. + • {adapter} (string|nil) + The VCS adapter: `"git"`, `"hg"`, `"jj"` or `"p4"`. + It determines how {rev.commit} is interpreted. + `nil` outside a repository (e.g. + |:DiffviewDiffFiles|). {diff_buf_win_enter} (`fun(bufnr: integer, winid: integer, ctx: table)`) Emitted after a diff buffer is displayed in a window. @@ -1812,12 +1837,7 @@ hooks *diffview-config-hooks* The window id of window inside which the buffer was displayed. {ctx} (`table`) - • {symbol} (string) - A symbol that identifies the window's position in - the layout. These symbols correspond with the - figures under |diffview-config-view.x.layout|. - • {layout_name} (string) - The name of the current layout. + See |diffview-diff-buf-ctx|. Examples: >lua hooks = { @@ -2501,6 +2521,9 @@ For information on how to use User autocommands, see |User|. such that |:setlocal| will apply settings to the relevant buffer / window. + The autocommand's `data` holds the fields of + |diffview-diff-buf-ctx| and `bufnr`. + `DiffviewDiffBufWinEnter` Fires after a diff buffer is displayed in a window. @@ -2509,6 +2532,9 @@ For information on how to use User autocommands, see |User|. such that |:setlocal| will apply settings to the relevant buffer / window. + The autocommand's `data` holds the fields of + |diffview-diff-buf-ctx|, `bufnr` and `winid`. + `DiffviewFilesStaged` Fires after files have been staged or unstaged from within a DiffView (e.g. via the stage/unstage actions). diff --git a/lua/diffview/config.lua b/lua/diffview/config.lua index 6f5c9e42..5da36cd5 100644 --- a/lua/diffview/config.lua +++ b/lua/diffview/config.lua @@ -649,9 +649,19 @@ M.defaults = { include_bare = false, }, + ---@class DiffviewDiffBufCtx.Rev + ---@field type "LOCAL"|"COMMIT"|"STAGE"|"CUSTOM" What the buffer holds: the working tree, a commit, an index stage, or content outside any VCS. + ---@field commit? string Full object name of the commit, for `COMMIT`. + ---@field stage? integer Index stage (0 index, 1 common ancestor, 2 ours, 3 theirs), for `STAGE`. + ---@field tracks_head boolean The rev was resolved from HEAD and follows it when HEAD moves. + ---@class DiffviewDiffBufCtx ---@field symbol string Layout-window symbol ("a"|"b"|"c"|"d"). ---@field layout_name string Concrete layout, e.g. "diff2_horizontal". + ---@field rev DiffviewDiffBufCtx.Rev The revision the buffer shows. + ---@field path string File path relative to `toplevel`. + ---@field toplevel string Root of the repository. Outside a repository, the directory `path` is relative to. + ---@field adapter? "git"|"hg"|"jj"|"p4" VCS adapter, which determines how `rev.commit` is interpreted. Nil outside a repository (e.g. `:DiffviewDiffFiles`). ---@class DiffviewHooks ---@field view_opened? fun(view: View) diff --git a/lua/diffview/init.lua b/lua/diffview/init.lua index e2302745..f63e651f 100644 --- a/lua/diffview/init.lua +++ b/lua/diffview/init.lua @@ -125,11 +125,19 @@ function M.init() DiffviewGlobal.emitter:on("view_post_layout", function(_) api.nvim_exec_autocmds("User", { pattern = "DiffviewViewPostLayout", modeline = false }) end) - DiffviewGlobal.emitter:on("diff_buf_read", function(_) - api.nvim_exec_autocmds("User", { pattern = "DiffviewDiffBufRead", modeline = false }) + DiffviewGlobal.emitter:on("diff_buf_read", function(_, bufnr, ctx) + api.nvim_exec_autocmds("User", { + pattern = "DiffviewDiffBufRead", + modeline = false, + data = vim.tbl_extend("force", ctx, { bufnr = bufnr }), + }) end) - DiffviewGlobal.emitter:on("diff_buf_win_enter", function(_) - api.nvim_exec_autocmds("User", { pattern = "DiffviewDiffBufWinEnter", modeline = false }) + DiffviewGlobal.emitter:on("diff_buf_win_enter", function(_, bufnr, winid, ctx) + api.nvim_exec_autocmds("User", { + pattern = "DiffviewDiffBufWinEnter", + modeline = false, + data = vim.tbl_extend("force", ctx, { bufnr = bufnr, winid = winid }), + }) end) DiffviewGlobal.emitter:on("selection_changed", function(_) api.nvim_exec_autocmds("User", { pattern = "DiffviewSelectionChanged", modeline = false }) diff --git a/lua/diffview/scene/window.lua b/lua/diffview/scene/window.lua index bc321532..b273bb08 100644 --- a/lua/diffview/scene/window.lua +++ b/lua/diffview/scene/window.lua @@ -271,10 +271,7 @@ Window.open_file = async.void(function(self) self.emitter:emit("post_open") api.nvim_win_call(self.id, function() - DiffviewGlobal.emitter:emit("diff_buf_win_enter", self.file.bufnr, self.id, { - symbol = self.file.symbol, - layout_name = self.parent.name, - }) + DiffviewGlobal.emitter:emit("diff_buf_win_enter", self.file.bufnr, self.id, self:diff_buf_ctx()) end) ---@diagnostic enable: invisible end) @@ -299,6 +296,28 @@ function Window:show_winbar_info() return false end +---Describe the diff buffer shown in this window, for the `diff_buf_read` and +---`diff_buf_win_enter` hooks and their `User` autocmds. +---@return DiffviewDiffBufCtx +function Window:diff_buf_ctx() + local file = self.file + local rev = file.rev + + return { + symbol = file.symbol, + layout_name = self.parent.name, + rev = { + type = RevType[rev.type], + commit = rev.commit, + stage = rev.stage, + tracks_head = rev.track_head, + }, + path = file.path, + toplevel = file.adapter.ctx.toplevel, + adapter = file.adapter.config_key, + } +end + function Window:is_nulled() return self:is_valid() and api.nvim_win_get_buf(self.id) == File.NULL_FILE.bufnr end diff --git a/lua/diffview/tests/functional/window_spec.lua b/lua/diffview/tests/functional/window_spec.lua index d2a892e4..6f67d600 100644 --- a/lua/diffview/tests/functional/window_spec.lua +++ b/lua/diffview/tests/functional/window_spec.lua @@ -176,6 +176,37 @@ describe("diffview.scene.window", function() end) end) + describe("diff_buf_ctx", function() + it("describes the buffer's position, revision and repository", function() + local adapter = mock_adapter({ config_key = "git" }) + local win = make_window(adapter, { + path = "lua/foo.lua", + symbol = "a", + rev = GitRev(RevType.COMMIT, "abc1234", true), + }) + win.parent = { name = "diff2_horizontal" } + + assert.same({ + symbol = "a", + layout_name = "diff2_horizontal", + rev = { type = "COMMIT", commit = "abc1234", tracks_head = true }, + path = "lua/foo.lua", + toplevel = adapter.ctx.toplevel, + adapter = "git", + }, win:diff_buf_ctx()) + end) + + it("reports the stage of an index revision", function() + local win = make_window(mock_adapter(), { + symbol = "b", + rev = GitRev(RevType.STAGE, 2), + }) + win.parent = { name = "diff3_mixed" } + + assert.same({ type = "STAGE", stage = 2, tracks_head = false }, win:diff_buf_ctx().rev) + end) + end) + describe("open_file view.foldlevel", function() -- Regression (#121): `view.foldlevel` must be copied onto the window's -- `foldlevel` option when the diff buffer is shown, even if the file's