Skip to content

feat(explorer): add optional Git line statistics - #491

Merged
esmuellert merged 2 commits into
esmuellert:mainfrom
richardgill:explorer-git-line-stats
Aug 1, 2026
Merged

feat(explorer): add optional Git line statistics#491
esmuellert merged 2 commits into
esmuellert:mainfrom
richardgill:explorer-git-line-stats

Conversation

@richardgill

@richardgill richardgill commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Adds opt-in Git line statistics to the Explorer.

  • works across all Explorer comparison modes:
    • working tree status
    • a revision against the working tree
    • two revisions against each other
  • handles staged, unstaged, renamed, deleted, binary, conflicting, and optionally bounded untracked files
  • preserves path filtering when calculating statistics
  • exposes per-file and aggregate folder/group statistics to the file, folder, and group formatter functions
  • includes sensible default formatter functions:
    • right-aligned, truncatable file statistics
    • aggregate group totals

Screenshots

Before

require("codediff").setup({
  explorer = {
    width = 56,
    view_mode = "tree",
    flatten_dirs = false,
  },
})

Before

After - disabled by default

require("codediff").setup({
  explorer = {
    width = 56,
    view_mode = "tree",
    flatten_dirs = false,
    -- line_stats is omitted because it is disabled by default.
  },
})

After disabled

After - enabled with default formatters

require("codediff").setup({
  explorer = {
    width = 56,
    view_mode = "tree",
    flatten_dirs = false,
    line_stats = {
      enabled = true,
      count_untracked = true,
      max_untracked_bytes = 1024 * 1024,
    },
  },
})

After enabled

After - custom file, folder, and group formatters

local default_formatters = require("codediff.ui.explorer.formatters")

require("codediff").setup({
  explorer = {
    width = 56,
    view_mode = "tree",
    flatten_dirs = false,
    line_stats = {
      enabled = true,
      count_untracked = true,
      max_untracked_bytes = 1024 * 1024,
    },
    formatters = {
      file = function(ctx)
        local layout = default_formatters.file(ctx)
        local right = {}

        if ctx.stats then
          local segments = ctx.stats.binary and {
            { text = "binary ", hl = "CodeDiffExplorerStatBinary" },
          } or {
            { text = "" .. ctx.stats.insertions, hl = "CodeDiffExplorerStatInsertions" },
            { text = "" .. ctx.stats.deletions .. " ", hl = "CodeDiffExplorerStatDeletions" },
          }
          right[#right + 1] = {
            segments = segments,
            truncate_priority = 3,
          }
        end

        right[#right + 1] = {
          segments = {
            { text = ctx.status, hl = ctx.status_hl },
            { text = string.rep(" ", ctx.status_right_margin), hl = "Normal" },
          },
        }
        layout.right = right
        return layout
      end,

      folder = function(ctx)
        local layout = default_formatters.folder(ctx)
        if not ctx.stats then
          return layout
        end

        layout.right = {
          {
            segments = {
              { text = "" .. ctx.stats.insertions, hl = "CodeDiffExplorerStatInsertions" },
              { text = "" .. ctx.stats.deletions, hl = "CodeDiffExplorerStatDeletions" },
            },
          },
        }
        return layout
      end,

      group = function(ctx)
        return {
          left = {
            {
              segments = { { text = " " .. ctx.label, hl = "CodeDiffExplorerTreeGroup" } },
              truncate_priority = 1,
            },
          },
          right = {
            {
              segments = {
                { text = ctx.file_count .. " files · ", hl = "CodeDiffExplorerStatFiles" },
                { text = "" .. ctx.stats.insertions, hl = "CodeDiffExplorerStatInsertions" },
                { text = "" .. ctx.stats.deletions, hl = "CodeDiffExplorerStatDeletions" },
              },
            },
          },
          min_gap = 2,
        }
      end,
    },
  },
})

After custom formatters

Supercedes part of #448.

Fixes #181

@richardgill
richardgill marked this pull request as ready for review July 29, 2026 06:50
@richardgill richardgill added the Size/M Medium - Write logic, add tests (one evening) label Jul 29, 2026
Move lua/codediff/ui/explorer/formatters.lua into
lua/codediff/ui/explorer/formatters/ so the built-in default row
functions and their helpers each live in their own file:

  init.lua    module entry; re-exports M.{file,folder,group}
  common.lua  shared prefix() helper (always active)
  stats.lua   line-stats rendering helpers (active only when
              explorer.line_stats.enabled = true)
  file.lua    default file row
  folder.lua  default folder row
  group.lua   default group row

This matches the folder-module style used by the rest of
lua/codediff/ui/explorer/ and makes it visible from the directory
listing which pieces are always used vs. line-stats specific.

The public require path stays codediff.ui.explorer.formatters, so
nodes.lua, tests, README, and doc references are unchanged. Pure code
motion; no behavior change (89 tests pass across the affected specs:
line_stats, line_formatters, explorer_modules, explorer, explorer_staging,
explorer_file_filter, highlights, git_line_stats).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@esmuellert esmuellert left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff top-to-bottom: well-scoped, opt-in default, non-breaking (base git API untouched, new highlight groups use default = true, formatter contexts remain additive with stats = nil when disabled). Tests are thorough — 8 real-git integration tests + 5 UI tests covering staged/unstaged/rename/deletion/binary/conflicts/pathspec/disabled-parity/error paths.

Pushed one small refactor commit on top: split explorer/formatters.lua into a explorer/formatters/ folder module (init, common, stats, file, folder, group) so the line-stats bits and the always-active row helpers are visible from the directory listing. Pure code motion; public require path preserved; all affected specs still green.

Nice work — thanks for the clean implementation and the great test coverage.

@esmuellert
esmuellert enabled auto-merge August 1, 2026 02:31
@esmuellert
esmuellert merged commit c8c99dc into esmuellert:main Aug 1, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Size/M Medium - Write logic, add tests (one evening)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Show diff stats (+/-) next to files in explorer

2 participants