Skip to content
Open
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
57 changes: 47 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pile.nvim is a Neovim plugin that provides a vertical buffer sidebar, similar to

- Vertical sidebar listing all open buffers.
- Easily switch between buffers with keyboard shortcuts.
- **Git worktree visual separation** - Automatically groups buffers by git worktree with visual separators.
- Editable buffer names within the sidebar for quick renaming.(not implemented yet)
- Automatically updates file names when a buffer is renamed.(not implemented yet)
- LSP integration: Automatically updates import paths when a file is renamed.(not implemented yet)
Expand Down Expand Up @@ -61,23 +62,59 @@ Plug 'shabaraba/pile.nvim'

## Setup and Configuration

<!--

To configure pile.nvim, add the following setup function to your Neovim config.
To configure pile.nvim, add the following setup function to your Neovim config:

```lua
require('pile').setup({
-- Configuration options
width = 30, -- Width of the sidebar
highlight_current = true, -- Highlight the current buffer in the sidebar
keymaps = {
open_buffer = '<CR>', -- Keymap to open the buffer
close_sidebar = 'q', -- Keymap to close the sidebar
-- Debug settings
debug = {
enabled = false, -- Enable debug logging
level = "info", -- Log level: "error", "warn", "info", "debug", "trace"
},

-- Git worktree display settings
worktree = {
enabled = true, -- Enable worktree visual separation
separator = {
enabled = true, -- Show separator lines between worktrees
style = "─", -- Character to use for separator line
show_branch = true, -- Show branch/worktree name in separator
},
highlight = {
separator = {
fg = "#61AFEF", -- Blue color for separator
bold = true,
},
branch = {
fg = "#98C379", -- Green color for branch name
bold = true,
},
},
},
})
```

-->
### Git Worktree Visual Separation

When working with multiple git worktrees, pile.nvim automatically detects and groups buffers by their associated worktree. Each worktree group is separated by a visual separator line that displays the branch name.

**Example sidebar with worktrees:**
```
─────── main ───────
config.lua
init.lua
──── feature/ui ────
component.lua
styles.lua
```
Comment on lines +102 to +109
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.


You can customize:
- `worktree.enabled` - Enable/disable worktree grouping
- `worktree.separator.enabled` - Show/hide separator lines
- `worktree.separator.style` - Character used for separator line
- `worktree.separator.show_branch` - Display branch name in separator
- `worktree.highlight.separator.fg` - Color of separator line
- `worktree.highlight.branch.fg` - Color of branch name

## Key Features:

Expand Down
38 changes: 24 additions & 14 deletions lua/pile/buffers/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local globals = require('pile.globals')
local window = require('pile.windows')
local popup = require('pile.windows.popup')
local log = require('pile.log')
local git = require('pile.git')
local M = {}

local selected_buffer = nil
Expand Down Expand Up @@ -81,24 +82,29 @@ function M.get_list()
-- 2. ポップアップ、通知、特殊バッファでないこと
-- 3. oil.nvimの一時バッファでないこと
-- 4. 表示されているか、特定の条件を満たすバッファであること
if info.filename ~= "" and
info.buftype ~= 'popup' and
info.filetype ~= 'notify' and
if info.filename ~= "" and
info.buftype ~= 'popup' and
info.filetype ~= 'notify' and
info.buftype ~= 'nofile' and
not is_oil_temp_buffer(info.buf, info.name, info.filetype) and
(info.displayed or info.name:match("%.%w+$")) then -- 表示されているか、拡張子を持つファイル


-- Detect git worktree for this buffer
local worktree = git.get_worktree_for_file(info.name)

-- 同名ファイルの数をカウント
filenames[info.filename] = (filenames[info.filename] or 0) + 1
table.insert(buffer_list, {
buf = info.buf,
name = info.name,
filename = info.filename
table.insert(buffer_list, {
buf = info.buf,
name = info.name,
filename = info.filename,
worktree = worktree
})

-- デバッグ情報: 初期バッファ追加時
log.debug(string.format("追加したバッファ: buf=%d, name=%s, filename=%s",
info.buf, info.name, info.filename))
log.debug(string.format("追加したバッファ: buf=%d, name=%s, filename=%s, worktree=%s",
info.buf, info.name, info.filename,
worktree and git.get_worktree_display_name(worktree) or "none"))
end
end

Expand Down Expand Up @@ -236,10 +242,14 @@ local function open_selected_callback(choice, idx)
popup.unmount()
end

-- @param props {available_windows: list}
-- @param props {available_windows: list, line: number (optional)}
function M.open_selected(props)
local cursor = vim.api.nvim_win_get_cursor(globals.sidebar_win)
local line = cursor[1]
local line = props.line
if not line then
local cursor = vim.api.nvim_win_get_cursor(globals.sidebar_win)
line = cursor[1]
end

selected_buffer = M.get_list()[line]
if not selected_buffer then
print("No buffer selected.")
Expand Down
58 changes: 57 additions & 1 deletion lua/pile/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,63 @@ M.setup = function(opts)
},
},
}


-- Git worktree display settings
M.worktree = {
enabled = true, -- Enable worktree visual separation
separator = {
enabled = true, -- Show separator lines between worktrees
style = "─", -- Character to use for separator line
show_branch = true, -- Show branch/worktree name in separator
},
highlight = {
separator = {
fg = "#61AFEF", -- Blue color for separator
bold = true,
},
branch = {
fg = "#98C379", -- Green color for branch name
bold = true,
},
},
}

-- 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
end

-- その他の設定があれば追加
end

Expand Down
142 changes: 142 additions & 0 deletions lua/pile/git.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
local log = require('pile.log')
local M = {}

-- Cache for worktree information
local worktree_cache = nil
local cache_timestamp = 0
local cache_ttl = 5000 -- 5 seconds TTL

-- Parse git worktree list output
local function parse_worktree_list(output)
local worktrees = {}
local current_worktree = nil

for line in output:gmatch("[^\r\n]+") do
-- Worktree path line starts with "worktree "
if line:match("^worktree ") then
local path = line:match("^worktree (.+)$")
if path then
current_worktree = {
path = path,
head = nil,
branch = nil,
bare = false
}
table.insert(worktrees, current_worktree)
end
elseif current_worktree then
-- HEAD line
local head = line:match("^HEAD (.+)$")
if head then
current_worktree.head = head
end

-- Branch line
local branch = line:match("^branch refs/heads/(.+)$")
if branch then
current_worktree.branch = branch
end

-- Bare repo
if line:match("^bare$") then
current_worktree.bare = true
end
end
end

return worktrees
end

-- Get all git worktrees
function M.get_worktrees()
local current_time = vim.loop.now()

-- Return cached result if still valid
if worktree_cache and (current_time - cache_timestamp) < cache_ttl then
return worktree_cache
end

-- Execute git worktree list
local output = vim.fn.system("git worktree list --porcelain 2>/dev/null")

-- Check if git command succeeded
if vim.v.shell_error ~= 0 then
log.debug("git worktree list failed - not in a git repository or git not available")
worktree_cache = {}
cache_timestamp = current_time
return {}
end

local worktrees = parse_worktree_list(output)

-- Sort worktrees by path length (longer paths first) for proper matching
table.sort(worktrees, function(a, b)
return #a.path > #b.path
end)

log.debug(string.format("Found %d git worktrees", #worktrees))
for i, wt in ipairs(worktrees) do
log.debug(string.format(" [%d] path=%s, branch=%s, bare=%s",
i, wt.path, wt.branch or "detached", wt.bare))
end

worktree_cache = worktrees
cache_timestamp = current_time

return worktrees
end

-- Find which worktree a file belongs to
function M.get_worktree_for_file(filepath)
if not filepath or filepath == "" then
return nil
end

local worktrees = M.get_worktrees()

-- No worktrees found
if #worktrees == 0 then
return nil
end

-- Normalize the filepath
local normalized_path = vim.fn.fnamemodify(filepath, ":p")

-- Find the worktree that contains this file
-- We check longest paths first (already sorted)
for _, worktree in ipairs(worktrees) do
local wt_path = vim.fn.fnamemodify(worktree.path, ":p")

-- Check if the file is under this worktree's path
if normalized_path:sub(1, #wt_path) == wt_path then
return worktree
end
end

return nil
end

-- Get a display name for a worktree
function M.get_worktree_display_name(worktree)
if not worktree then
return "No Worktree"
end

if worktree.branch then
return worktree.branch
elseif worktree.head then
-- Show shortened commit hash for detached HEAD
return worktree.head:sub(1, 7)
else
-- Fallback to directory name
return vim.fn.fnamemodify(worktree.path, ":t")
end
end

-- Clear the worktree cache (useful for testing or manual refresh)
function M.clear_cache()
worktree_cache = nil
cache_timestamp = 0
end

return M
12 changes: 12 additions & 0 deletions lua/pile/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ function M.setup(opts)
fg = "White",
})

-- Git worktree separator highlight
if Config.worktree and Config.worktree.enabled then
vim.api.nvim_set_hl(0, "PileWorktreeSeparator", {
fg = Config.worktree.highlight.separator.fg,
bold = Config.worktree.highlight.separator.bold,
})
vim.api.nvim_set_hl(0, "PileWorktreeBranch", {
fg = Config.worktree.highlight.branch.fg,
bold = Config.worktree.highlight.branch.bold,
})
end

vim.api.nvim_create_user_command("PileToggle", M.toggle_sidebar, { desc = "toggle pile window" })
vim.api.nvim_create_user_command("PileGoToNextBuffer", M.switch_to_next_buffer, { desc = "go to next buffer" })
vim.api.nvim_create_user_command("PileGoToPrevBuffer", M.switch_to_prev_buffer, { desc = "go to prev buffer" })
Expand Down
Loading