Conversation
This commit implements visual separation for git worktrees in the pile.nvim sidebar. When working with multiple git worktrees, buffers are now automatically grouped by their associated worktree, with visual separators displaying the branch name. Key changes: - Added git.lua module for worktree detection using `git worktree list` - Modified buffers/init.lua to detect worktree for each buffer - Updated config.lua with comprehensive worktree display options - Modified sidebar.lua to group buffers by worktree and display separators - Added line_to_buffer mapping to handle separator lines in keybindings - Added PileWorktreeSeparator and PileWorktreeBranch highlight groups - Updated README.md with worktree feature documentation and configuration examples Features: - Automatic worktree detection with caching (5s TTL) - Configurable separator style and colors - Optional branch name display in separators - Proper handling of buffers not in any worktree - Separator lines are non-interactive (no action on Enter/dd) Configuration: - worktree.enabled - Enable/disable feature - worktree.separator.enabled - Show/hide separators - worktree.separator.style - Customize separator character - worktree.separator.show_branch - Toggle branch name display - worktree.highlight.* - Customize colors Fixes #42
📝 WalkthroughWalkthroughThis PR implements git worktree visual separation for the sidebar buffer list. It introduces a new git module for worktree detection and caching, extends buffer metadata to include worktree information, adds configuration options for worktree display customization, and refactors the sidebar to render buffers grouped by worktree with visual separators between groups. Changes
Sequence DiagramsequenceDiagram
participant User as User / Editor
participant Sidebar as Sidebar Component
participant Config as Configuration
participant Buffers as Buffer Manager
participant Git as Git Module
User->>Config: Load pile setup with worktree config
Config->>Config: Initialize worktree settings (separator, highlight)
Sidebar->>Buffers: Request buffer list
Buffers->>Git: For each buffer, get_worktree_for_file()
Git->>Git: Execute "git worktree list --porcelain"
Git->>Git: Cache worktree info (TTL-based)
Git-->>Buffers: Return worktree object (path, branch, etc.)
Buffers->>Buffers: Attach worktree to buffer entry
Buffers-->>Sidebar: Return buffers with worktree metadata
Sidebar->>Sidebar: Group buffers by worktree
Sidebar->>Sidebar: Insert separators between groups
Sidebar->>Sidebar: Build display lines with grouping
Sidebar->>Sidebar: Create line_to_buffer mapping
Sidebar->>Config: Apply worktree highlight groups
Sidebar-->>User: Render sidebar with grouped buffers + separators
User->>Sidebar: Press Enter on buffer line
Sidebar->>Sidebar: Map display line to buffer via line_to_buffer
Sidebar->>Buffers: M.open_selected(props with line)
Buffers-->>User: Open selected buffer
User->>Sidebar: Press 'dd' on buffer line
Sidebar->>Sidebar: Map display line to buffer (skip separator)
Sidebar->>Buffers: Delete buffer
Buffers-->>User: Buffer deleted
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lua/pile/windows/sidebar.lua (1)
142-190: Fix reversed visual selections and log level in delete keymaps.
- Visual delete fails when selection goes backward:
vim.fn.getpos('v')[2]can be greater thanvim.fn.getpos('.')[2], causing the loop to never execute. Usemath.min/maxto normalize the range.ddlogs atinfolevel on every keypress; change todebug.Suggested fixes
vim.keymap.set('n', 'dd', function() @@ - log.info(string.format("Current line: %d, buffer_idx: %s", current_line, buffer_idx or "none")) + log.debug(string.format("Current line: %d, buffer_idx: %s", current_line, buffer_idx or "none")) @@ vim.keymap.set('x', 'd', function() local start_line = vim.fn.getpos('v')[2] local end_line = vim.fn.getpos('.')[2] - for line = start_line, end_line do + local lo = math.min(start_line, end_line) + local hi = math.max(start_line, end_line) + for line = lo, hi do local buffer_idx = line_to_buffer[line] if buffer_idx then local selected_buffer = buffer_list[buffer_idx] if selected_buffer then vim.api.nvim_buf_delete(selected_buffer.buf, { force = true }) end end end
🤖 Fix all issues with AI agents
In @README.md:
- Around line 102-109: The fenced code block showing the sidebar visualization
in README.md is missing a language specifier; update the opening fence for that
block (the triple backticks before the lines "─────── main ───────",
"config.lua", "init.lua", etc.) to use "text" (i.e., change ``` to ```text) so
the example is explicitly rendered as plain text.
🧹 Nitpick comments (5)
lua/pile/init.lua (1)
43-53: LGTM! Consider adding defensive nil checks.The conditional highlighting logic is correct and properly guarded. Since
Config.setup(opts)initializes all default values before this block executes, the nested field access should be safe. However, for extra defensive programming, you could add nil checks for the highlight subfields.🛡️ Optional: Add defensive nil checks
-- Git worktree separator highlight if Config.worktree and Config.worktree.enabled then + local sep_hl = Config.worktree.highlight and Config.worktree.highlight.separator or {} + local branch_hl = Config.worktree.highlight and Config.worktree.highlight.branch or {} vim.api.nvim_set_hl(0, "PileWorktreeSeparator", { - fg = Config.worktree.highlight.separator.fg, - bold = Config.worktree.highlight.separator.bold, + fg = sep_hl.fg, + bold = sep_hl.bold, }) vim.api.nvim_set_hl(0, "PileWorktreeBranch", { - fg = Config.worktree.highlight.branch.fg, - bold = Config.worktree.highlight.branch.bold, + fg = branch_hl.fg, + bold = branch_hl.bold, }) endlua/pile/config.lua (1)
70-104: Refactor user config override logic to reduce boilerplate.The current approach uses 35 lines of nested if statements to override user-provided worktree configuration. This can be simplified significantly using Lua's standard pattern or Neovim's
vim.tbl_deep_extend.♻️ Proposed refactor using vim.tbl_deep_extend
-- Apply user-provided worktree settings if opts and opts.worktree then - if opts.worktree.enabled ~= nil then - M.worktree.enabled = opts.worktree.enabled - end - if opts.worktree.separator then - if opts.worktree.separator.enabled ~= nil then - M.worktree.separator.enabled = opts.worktree.separator.enabled - end - if opts.worktree.separator.style ~= nil then - M.worktree.separator.style = opts.worktree.separator.style - end - if opts.worktree.separator.show_branch ~= nil then - M.worktree.separator.show_branch = opts.worktree.separator.show_branch - end - end - if opts.worktree.highlight then - if opts.worktree.highlight.separator then - if opts.worktree.highlight.separator.fg ~= nil then - M.worktree.highlight.separator.fg = opts.worktree.highlight.separator.fg - end - if opts.worktree.highlight.separator.bold ~= nil then - M.worktree.highlight.separator.bold = opts.worktree.highlight.separator.bold - end - end - if opts.worktree.highlight.branch then - if opts.worktree.highlight.branch.fg ~= nil then - M.worktree.highlight.branch.fg = opts.worktree.highlight.branch.fg - end - if opts.worktree.highlight.branch.bold ~= nil then - M.worktree.highlight.branch.bold = opts.worktree.highlight.branch.bold - end - end - end + M.worktree = vim.tbl_deep_extend("force", M.worktree, opts.worktree) endThis reduces the code from 35 lines to 1 line while preserving the same behavior.
vim.tbl_deep_extendrecursively merges tables, only overriding values that are explicitly provided by the user.lua/pile/buffers/init.lua (1)
92-93: Worktree detection is correct and cached.The worktree detection for each buffer is properly implemented and benefits from the git module's 5-second TTL cache. Performance should be acceptable for typical use cases.
⚡ Optional: Pre-fetch worktrees once before the loop
For a marginal performance improvement, you could pre-fetch worktrees once before the loop:
-- バッファをフィルタリングして一覧に追加 + local worktrees = git.get_worktrees() -- Pre-fetch once for _, info in ipairs(all_buffer_info) do -- デバッグ情報: 各バッファの詳細情報 log.debug(string.format("バッファ詳細: buf=%d, buftype=%s, filetype=%s, name=%s, filename=%s, displayed=%s",However, this optimization is minor since the git module already caches results.
lua/pile/git.lua (1)
60-60: Consider async git execution for improved responsiveness.The synchronous
vim.fn.systemcall could block the UI if git operations are slow. While acceptable for most cases (especially with 5s caching), consider usingvim.loop.spawnorvim.systemwith async callbacks for better responsiveness in future iterations.lua/pile/windows/sidebar.lua (1)
22-118: Avoid drift:set_buffer_lines()andM.update()duplicate the same grouping/mapping builder.
This is easy to get subtly inconsistent over time (and it already is: first-group separator behavior differs by comment/intention). Extract a shared helper that returns{ lines, line_to_buffer }.Also applies to: 231-326
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
README.mdlua/pile/buffers/init.lualua/pile/config.lualua/pile/git.lualua/pile/init.lualua/pile/windows/sidebar.lua
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
README.md
102-102: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (12)
lua/pile/config.lua (1)
50-68: LGTM! Well-structured configuration defaults.The worktree configuration block is well-organized with sensible defaults. The separator style using the box-drawing character "─" provides clean visual separation.
README.md (2)
19-19: LGTM! Feature mention is clear and concise.The feature list accurately describes the new git worktree visual separation capability.
65-95: LGTM! Comprehensive configuration example.The setup example clearly documents all available worktree configuration options with their default values, making it easy for users to customize the feature.
lua/pile/buffers/init.lua (2)
97-107: LGTM! Clean integration of worktree metadata.The worktree field is properly added to buffer entries, and the enhanced debug logging will be helpful for troubleshooting. The use of
git.get_worktree_display_namehandles nil values gracefully.
245-251: LGTM! Optional line parameter preserves backward compatibility.The implementation correctly handles the optional
lineparameter, falling back to cursor position when not provided. This maintains backward compatibility while supporting the new worktree separator functionality.lua/pile/git.lua (6)
4-7: LGTM! Simple and effective caching strategy.The cache design with a 5-second TTL is appropriate for worktree information, which changes infrequently. Module-level variables are suitable for this singleton cache pattern.
51-87: LGTM! Robust caching and error handling.The implementation properly caches both successful and failed git operations, preventing repeated failed calls. Sorting worktrees by path length (longest first) is essential for correct file-to-worktree matching when worktrees are nested.
90-117: LGTM! Correct path matching with proper normalization.The function correctly normalizes both file and worktree paths using
fnamemodifywith:p, ensuring platform-appropriate path separators. The longest-path-first matching strategy correctly handles nested worktrees by returning the most specific match.
120-134: LGTM! Well-designed display name logic.The function provides sensible display names with appropriate fallbacks: branch name (most common), short commit hash for detached HEAD (standard 7 characters), and directory name as last resort. All cases return a valid string.
137-140: LGTM! Simple cache clear utility.The function provides a clean way to manually clear the cache, useful for testing or forcing a refresh of worktree information.
10-48: Git worktree porcelain format parsing is correct.The parsing logic correctly handles the
git worktree list --porcelainformat, including edge cases like detached HEAD worktrees and bare repositories. All documented fields (path, HEAD hash, branch name, and bare flag) are properly parsed and the state machine logic is protected against nil dereferences.lua/pile/windows/sidebar.lua (1)
4-9: Good: explicit config/git deps + line→buffer mapping is the right shape for “non-interactive separators”.
One minor: consider makingline_to_buffera table you mutate (clear keys) rather than reassigning, if any other module ever captures the reference (today it doesn’t look like it).
| -- Check if worktree grouping is enabled | ||
| if config.worktree and config.worktree.enabled then | ||
| -- Group buffers by worktree | ||
| local worktree_groups = {} | ||
| local worktree_order = {} | ||
|
|
||
| for i, buffer in ipairs(buffer_list) do | ||
| local wt_key = "none" | ||
| if buffer.worktree then | ||
| wt_key = buffer.worktree.path | ||
| end | ||
|
|
||
| if not worktree_groups[wt_key] then | ||
| worktree_groups[wt_key] = { | ||
| worktree = buffer.worktree, | ||
| buffers = {}, | ||
| indices = {} | ||
| } | ||
| table.insert(worktree_order, wt_key) | ||
| end | ||
|
|
||
| table.insert(worktree_groups[wt_key].buffers, buffer) | ||
| table.insert(worktree_groups[wt_key].indices, i) | ||
| end | ||
|
|
||
| -- Build lines with worktree separators | ||
| local line_num = 1 | ||
| for group_idx, wt_key in ipairs(worktree_order) do | ||
| local group = worktree_groups[wt_key] | ||
|
|
||
| -- Add separator line if enabled and not the first group | ||
| if config.worktree.separator and config.worktree.separator.enabled then | ||
| local separator_line = "" | ||
|
|
||
| if config.worktree.separator.show_branch and group.worktree then | ||
| local branch_name = git.get_worktree_display_name(group.worktree) | ||
| local sep_char = config.worktree.separator.style or "─" | ||
| local max_width = 28 -- Leave some padding | ||
| local branch_text = " " .. branch_name .. " " | ||
| local remaining_width = max_width - #branch_text | ||
|
|
||
| if remaining_width > 0 then | ||
| local left_width = math.floor(remaining_width / 2) | ||
| local right_width = remaining_width - left_width | ||
| separator_line = string.rep(sep_char, left_width) .. branch_text .. string.rep(sep_char, right_width) | ||
| else | ||
| separator_line = branch_text:sub(1, max_width) | ||
| end | ||
| else | ||
| separator_line = string.rep(config.worktree.separator.style or "─", 28) | ||
| end | ||
|
|
||
| table.insert(lines, separator_line) | ||
| -- Separator lines don't map to any buffer | ||
| line_to_buffer[line_num] = nil | ||
| line_num = line_num + 1 | ||
| end | ||
|
|
||
| -- Add buffer lines for this worktree | ||
| for buf_idx, buffer in ipairs(group.buffers) do | ||
| table.insert(lines, buffer.filename) | ||
| -- Map this line to the original buffer index | ||
| line_to_buffer[line_num] = group.indices[buf_idx] | ||
| line_num = line_num + 1 | ||
| end | ||
| end | ||
| else |
There was a problem hiding this comment.
Separator logic/width: currently adds a separator before the first group + hard-codes 28 columns.
set_buffer_lines()comment says “not the first group”, butgroup_idxisn’t used (so the first line becomes a separator).max_width = 28assumes the sidebar is 30 columns forever; users can resize or configs may change.#branch_textcounts bytes, not display width (branch names / unicode can misalign).
Proposed fix (apply similarly in both functions)
- -- Add separator line if enabled and not the first group
- if config.worktree.separator and config.worktree.separator.enabled then
+ -- Add separator line between groups
+ if config.worktree.separator
+ and config.worktree.separator.enabled
+ and group_idx > 1
+ then
local separator_line = ""
if config.worktree.separator.show_branch and group.worktree then
local branch_name = git.get_worktree_display_name(group.worktree)
local sep_char = config.worktree.separator.style or "─"
- local max_width = 28 -- Leave some padding
+ local win_width = (globals.sidebar_win and vim.api.nvim_win_is_valid(globals.sidebar_win))
+ and vim.api.nvim_win_get_width(globals.sidebar_win)
+ or 30
+ local max_width = math.max(1, win_width - 2) -- padding
local branch_text = " " .. branch_name .. " "
- local remaining_width = max_width - #branch_text
+ local remaining_width = max_width - vim.fn.strdisplaywidth(branch_text)
if remaining_width > 0 then
local left_width = math.floor(remaining_width / 2)
local right_width = remaining_width - left_width
separator_line = string.rep(sep_char, left_width) .. branch_text .. string.rep(sep_char, right_width)
else
separator_line = branch_text:sub(1, max_width)
end
else
- separator_line = string.rep(config.worktree.separator.style or "─", 28)
+ separator_line = string.rep(config.worktree.separator.style or "─", max_width)
end
table.insert(lines, separator_line)
- -- Separator lines don't map to any buffer
- line_to_buffer[line_num] = nil
line_num = line_num + 1
endAlso applies to: 249-315
| local function highlight_buffer(target_buffer) | ||
| buffer_list = buffers.get_list() | ||
| for i, buffer in ipairs(buffer_list) do | ||
| if buffer.buf == target_buffer then | ||
| vim.api.nvim_buf_add_highlight(globals.sidebar_buf, -1, "SidebarCurrentBuffer", i - 1, 0, -1) | ||
|
|
||
| -- Find which display line corresponds to the target buffer | ||
| for display_line, buffer_idx in pairs(line_to_buffer) do | ||
| if buffer_idx and buffer_list[buffer_idx] and buffer_list[buffer_idx].buf == target_buffer then | ||
| vim.api.nvim_buf_add_highlight(globals.sidebar_buf, -1, "SidebarCurrentBuffer", display_line - 1, 0, -1) | ||
| end | ||
| end | ||
|
|
||
| -- Apply worktree separator highlighting if enabled | ||
| if config.worktree and config.worktree.enabled and config.worktree.separator and config.worktree.separator.enabled then | ||
| for display_line = 1, vim.api.nvim_buf_line_count(globals.sidebar_buf) do | ||
| if not line_to_buffer[display_line] then | ||
| -- This is a separator line | ||
| vim.api.nvim_buf_add_highlight(globals.sidebar_buf, -1, "PileWorktreeSeparator", display_line - 1, 0, -1) | ||
| end | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Highlighting leaks namespaces across updates (ns_id = -1) and never clears old highlights.
Calling nvim_buf_add_highlight(..., -1, ...) repeatedly creates a new namespace every time; over many updates this can degrade performance/memory. Use one namespace + clear it on each redraw.
Proposed fix
local M = {}
+local hl_ns = vim.api.nvim_create_namespace("pile.sidebar")
local function highlight_buffer(target_buffer)
buffer_list = buffers.get_list()
+ vim.api.nvim_buf_clear_namespace(globals.sidebar_buf, hl_ns, 0, -1)
-- Find which display line corresponds to the target buffer
for display_line, buffer_idx in pairs(line_to_buffer) do
if buffer_idx and buffer_list[buffer_idx] and buffer_list[buffer_idx].buf == target_buffer then
- vim.api.nvim_buf_add_highlight(globals.sidebar_buf, -1, "SidebarCurrentBuffer", display_line - 1, 0, -1)
+ vim.api.nvim_buf_add_highlight(globals.sidebar_buf, hl_ns, "SidebarCurrentBuffer", display_line - 1, 0, -1)
end
end
-- Apply worktree separator highlighting if enabled
if config.worktree and config.worktree.enabled and config.worktree.separator and config.worktree.separator.enabled then
for display_line = 1, vim.api.nvim_buf_line_count(globals.sidebar_buf) do
if not line_to_buffer[display_line] then
- vim.api.nvim_buf_add_highlight(globals.sidebar_buf, -1, "PileWorktreeSeparator", display_line - 1, 0, -1)
+ vim.api.nvim_buf_add_highlight(globals.sidebar_buf, hl_ns, "PileWorktreeSeparator", display_line - 1, 0, -1)
end
end
end
endAnd in M.update() (before adding highlights):
+ vim.api.nvim_buf_clear_namespace(globals.sidebar_buf, hl_ns, 0, -1)Also applies to: 334-355
| ``` | ||
| ─────── main ─────── | ||
| config.lua | ||
| init.lua | ||
| ──── feature/ui ──── | ||
| component.lua | ||
| styles.lua | ||
| ``` |
There was a problem hiding this comment.
Add language specifier to fenced code block.
The example sidebar visualization is helpful, but the fenced code block is missing a language specifier. Add "text" to clarify it's plain text output.
📝 Proposed fix
**Example sidebar with worktrees:**
-```
+```text
─────── main ───────
config.lua
init.lua🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
102-102: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In @README.md around lines 102 - 109, The fenced code block showing the sidebar
visualization in README.md is missing a language specifier; update the opening
fence for that block (the triple backticks before the lines "─────── main
───────", "config.lua", "init.lua", etc.) to use "text" (i.e., change ``` to
```text) so the example is explicitly rendered as plain text.
This commit implements visual separation for git worktrees in the pile.nvim sidebar. When working with multiple git worktrees, buffers are now automatically grouped by their associated worktree, with visual separators displaying the branch name.
Key changes:
git worktree listFeatures:
Configuration:
Fixes #42
Summary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.