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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ All plugin code lives under `lua/fude/`. The plugin entry point is `plugin/fude.
- **`ui/format.lua`** — Pure format/calculation functions with no state or vim API side effects: `calculate_float_dimensions`, `format_comments_for_display`, `normalize_check`, `format_check_status`, `deduplicate_checks`, `sort_checks`, `build_checks_summary`, `format_review_status`, `build_reviewers_list`, `build_reviewers_summary`, `calculate_overview_layout`, `calculate_comments_height`, `calculate_reply_window_dimensions`, `format_reply_comments_for_display`, `build_overview_left_lines`, `build_overview_right_lines`, `calculate_comment_browser_layout`, `format_comment_browser_list`, `format_comment_browser_thread`, `parse_markdown_line`, `build_highlighted_chunks`, `apply_markdown_highlight_to_line`, `normalize_newlines`. Purity is enforced by `scripts/check_purity.lua` (`make check-purity`).
- **`ui/inline.lua`** — Inline (virt_lines) comment box rendering. `format_comments_for_inline(comments, format_date_fn, opts)` builds bordered comment boxes for use as `nvim_buf_set_extmark` `virt_lines`. Reads the current window's text width via `vim.api`/`vim.fn` to size boxes, which is why it lives outside the pure `ui/format.lua`.
- **`ui/comment_browser.lua`** — 3-pane floating comment browser for `FudeReviewListComments`. Left pane: comment list (review + PR-level, time-descending), including local drafts via `merge_draft_entries` (existing entries get a `✎draft` marker; new line/suggest/issue drafts appear as `[draft]` rows). Selecting a `type="draft"` row: `line`/`suggest` jumps to the file (`<CR>`), `issue` focuses the lower pane (prefilled). Right upper: thread display (draft body preview for draft rows). Right lower: reply/edit/new comment input. Supports reply, edit, delete, new PR comment, jump to file, and refresh. Does not depend on Telescope.
- **`ui/sidepanel.lua`** — Toggleable sidebar showing Review Scope and Changed Files. Files default to a flat list (`sidepanel.file_tree = "flat"`); `"tree"` groups files by directory via `ui/sidepanel/tree.lua`. Pure functions: `format_scope_section`, `format_files_section`, `format_files_section_tree`, `build_sidepanel_content`, `resolve_entry_at_cursor`. Side-effect functions: `open`, `close`, `toggle`, `refresh`, `toggle_file_tree_mode`. Uses `nvim_open_win` with `split` for sidebar creation. Uses dedicated `fude_sidepanel` namespace for highlights (avoids `refresh_extmarks` clearing them on BufEnter). Auto-refreshes on scope change and reload. Keymaps: `<CR>` select/open, `<Tab>` toggle reviewed/viewed, `t` toggle flat/tree, `R` reload, `q` close.
- **`ui/sidepanel.lua`** — Toggleable sidebar showing Review Scope and Changed Files. Files default to a flat list (`sidepanel.file_tree = "flat"`); `"tree"` groups files by directory via `ui/sidepanel/tree.lua`. Pure functions: `format_scope_section`, `format_files_section(file_entries, width, format_path_fn, viewed_count)`, `format_files_section_tree(tree_entries, total_file_count, width, viewed_count)`, `build_sidepanel_content`, `resolve_entry_at_cursor`. The Files section header displays viewed/total count (e.g., "Files (Reviewed: 3/10)"). Side-effect functions: `open`, `close`, `toggle`, `refresh`, `toggle_file_tree_mode`. Uses `nvim_open_win` with `split` for sidebar creation. Uses dedicated `fude_sidepanel` namespace for highlights (avoids `refresh_extmarks` clearing them on BufEnter). Auto-refreshes on scope change and reload. Keymaps: `<CR>` select/open, `<Tab>` toggle reviewed/viewed, `t` toggle flat/tree, `R` reload, `q` close.
- **`ui/sidepanel/tree.lua`** — Pure tree helpers for the sidepanel Files section: `build_tree`, `collapse_singleton_chains`, `compute_aggregate`, and `flatten_tree`.
- **`ui/extmarks.lua`** — Extmark management: `flash_line`, `highlight_comment_lines`, `clear_comment_line_highlight`, `refresh_extmarks`, `clear_extmarks`, `clear_all_extmarks`. Uses lazy `require("fude.comments")` to avoid circular dependencies. `refresh_extmarks` also renders the `draft` indicator from `drafts.file_markers(rel_path)` (EOL virt_text, both virtualText and inline modes).
- **`files.lua`** — Changed files display via Telescope picker, snacks.picker, or quickfix list. All pickers show diff preview and viewed state toggle via `<Tab>`. Shows GitHub viewed status for each file. Exports `apply_viewed_toggle(path, on_done)` as a picker-agnostic state mutator that invokes gh GraphQL mark/unmark, updates `state.viewed_files`, and calls `on_done` with the updated display fields; the Telescope adapter `toggle_viewed_in_telescope` and the snacks adapter `toggle_viewed_in_snacks(picker, item)` both delegate to it. `show()` routes to `show_telescope` / `show_snacks` / `show_quickfix` based on `config.opts.file_list_mode`; snacks falls back to quickfix when snacks.nvim is missing. Also exports `next_file()` / `prev_file()` for jumping between changed files (used by `:FudeReviewNextFile` / `:FudeReviewPrevFile`); both wrap around at the edges and fall back to the first/last entry when the current buffer is not part of the PR. The pure helper `find_adjacent_file_index(changed_files, current_path, direction)` computes the target index.
- **`files.lua`** — Changed files display via Telescope picker, snacks.picker, or quickfix list. All pickers show diff preview and viewed state toggle via `<Tab>`. Shows GitHub viewed status for each file. Exports `apply_viewed_toggle(path, on_done)` as a picker-agnostic state mutator that invokes gh GraphQL mark/unmark, updates `state.viewed_files`, and calls `on_done` with the updated display fields; the Telescope adapter `toggle_viewed_in_telescope` and the snacks adapter `toggle_viewed_in_snacks(picker, item)` both delegate to it. `show()` routes to `show_telescope` / `show_snacks` / `show_quickfix` based on `config.opts.file_list_mode`; snacks falls back to quickfix when snacks.nvim is missing. Also exports `next_file()` / `prev_file()` for jumping between changed files (used by `:FudeReviewNextFile` / `:FudeReviewPrevFile`); both wrap around at the edges and fall back to the first/last entry when the current buffer is not part of the PR. The pure helpers `find_adjacent_file_index(changed_files, current_path, direction)` computes the target index; `count_viewed(viewed_files, changed_files)` counts files with VIEWED state (used by sidepanel header).
- **`scope.lua`** — Review scope selection and navigation. Provides a Telescope picker (or `vim.ui.select` fallback) for choosing between full PR scope and individual commit scope, with commit index display (`[1/10]`) and current scope marker (`▶`). Supports next/prev scope navigation (`next_scope`/`prev_scope`), marking commits as reviewed via `<Tab>` in the Telescope picker (tracked locally in `state.reviewed_commits`), and statusline integration (`statusline()`). On commit scope: checks out the commit, fetches commit-specific changed files, updates gitsigns base to `sha^` (global), and refreshes the diff preview. On full PR scope: restores the original HEAD, re-fetches PR-wide changed files, and computes merge-base (per-buffer gitsigns base is applied via `GitSignsUpdate` autocmd in init.lua). Exports `apply_reviewed_toggle(sha)` as a picker-agnostic state mutator that toggles `state.reviewed_commits[sha]` and returns the updated display fields `{ is_reviewed, reviewed_icon, reviewed_hl }`; both the Telescope adapter `toggle_reviewed_in_telescope` and the snacks adapter `toggle_reviewed_in_snacks(picker, item)` delegate to it. `select_scope()` routes to `show_telescope` / `show_snacks` / `show_vim_select` based on `config.opts.file_list_mode`; snacks falls back to `vim.ui.select` when snacks.nvim is missing.
- **`overview.lua`** — PR overview display: fetches extended PR info and issue-level comments, renders in a centered float with keymaps for commenting and refreshing.
- **`completion/init.lua`** — Completion source for comment input buffers. Provides mention (`@user`), issue/PR (`#nnn`), and commit SHA completion candidates via `fetch_mentions`, `fetch_issues`, `fetch_commits`, with a 5-minute TTL cache. `build_commit_items(commit_entries)` is a pure helper that formats commit entries into completion items; `get_context(line_before_cursor)` parses the trigger character (`@`/`#`/sha prefix). Reads `state.pr_commits` (only) for commit completion freshness invalidation.
Expand Down
3 changes: 2 additions & 1 deletion doc/fude.txt
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,8 @@ Using lazy.nvim: >lua

Files:~
Lists all changed files in the current scope with status icons,
additions/deletions counts, and viewed state.
additions/deletions counts, and viewed state. The header shows
the viewed file count out of total (e.g., "Files (Reviewed: 3/10)").

Keymaps (panel):~
`<CR>` Select scope entry or open file
Expand Down
15 changes: 15 additions & 0 deletions lua/fude/files.lua
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ function M.comment_count_display(submitted, pending, outdated)
return "💬" .. total, hl
end

--- Count files with VIEWED state among changed files.
--- @param viewed_files table<string, string>|nil { [path] = "VIEWED" | "UNVIEWED" | "DISMISSED" }
--- @param changed_files table[] list of { path, ... }
--- @return number
function M.count_viewed(viewed_files, changed_files)
viewed_files = viewed_files or {}
local count = 0
for _, file in ipairs(changed_files) do
if viewed_files[file.path] == "VIEWED" then
count = count + 1
end
end
return count
end

--- Build normalized file entries from changed files list.
--- @param changed_files table[] list of { path, status, additions, deletions, patch }
--- @param repo_root string repository root directory
Expand Down
18 changes: 12 additions & 6 deletions lua/fude/ui/sidepanel.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,16 @@ end
--- @param file_entries table[] entries from files.build_file_entries
--- @param width number available width in columns
--- @param format_path_fn (fun(s: string): string|nil)|nil formats file path for display (nil = identity)
--- @param viewed_count number count of files with VIEWED state
--- @return string[] lines
--- @return table[] highlights { { line_0idx, col_start, col_end, hl_group } }
--- @return number entry_count number of file entries
function M.format_files_section(file_entries, width, format_path_fn)
function M.format_files_section(file_entries, width, format_path_fn, viewed_count)
format_path_fn = format_path_fn or function(p)
return p
end
local lines = { string.format(" Files (%d)", #file_entries), string.rep("─", width) }
viewed_count = viewed_count or 0
local lines = { string.format(" Files (Reviewed: %d/%d)", viewed_count, #file_entries), string.rep("─", width) }
Comment on lines +63 to +68

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

b7b2a74 で修正 — viewed_count = viewed_count or 0 を追加しました

local highlights = {
{ 0, 0, -1, "Title" },
}
Expand Down Expand Up @@ -99,11 +101,13 @@ end
--- @param tree_entries table[] entries from ui.sidepanel.tree.flatten_tree
--- @param total_file_count number total number of changed files
--- @param width number available width in columns
--- @param viewed_count number count of files with VIEWED state
--- @return string[] lines
--- @return table[] highlights { { line_0idx, col_start, col_end, hl_group } }
--- @return number entry_count number of rendered tree entries
function M.format_files_section_tree(tree_entries, total_file_count, width)
local lines = { string.format(" Files (%d)", total_file_count), string.rep("─", width) }
function M.format_files_section_tree(tree_entries, total_file_count, width, viewed_count)
viewed_count = viewed_count or 0
local lines = { string.format(" Files (Reviewed: %d/%d)", viewed_count, total_file_count), string.rep("─", width) }
local highlights = {
{ 0, 0, -1, "Title" },
}
Expand Down Expand Up @@ -269,6 +273,7 @@ local function render(panel)
-- Build file entries (skip if repo root unavailable)
local repo_root = diff_mod.get_repo_root()
local file_entries = {}
local viewed_count = 0
if repo_root then
local viewed_sign = (config.opts.signs and config.opts.signs.viewed) or "✓"
local comment_counts = comments_data.build_file_comment_counts(state.comments, state.pending_comments)
Expand All @@ -280,6 +285,7 @@ local function render(panel)
viewed_sign,
comment_counts
)
viewed_count = files_mod.count_viewed(state.viewed_files, state.changed_files or {})
end

-- Format sections
Expand All @@ -291,9 +297,9 @@ local function render(panel)
local tree = tree_mod.build_tree(file_entries)
tree_mod.collapse_singleton_chains(tree)
tree_entries = tree_mod.flatten_tree(tree, state.viewed_files)
file_lines, file_hls, file_count = M.format_files_section_tree(tree_entries, #file_entries, width)
file_lines, file_hls, file_count = M.format_files_section_tree(tree_entries, #file_entries, width, viewed_count)
else
file_lines, file_hls, file_count = M.format_files_section(file_entries, width, config.format_path)
file_lines, file_hls, file_count = M.format_files_section(file_entries, width, config.format_path, viewed_count)
end

local lines, highlights, section_map =
Expand Down
36 changes: 36 additions & 0 deletions tests/fude/files_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,42 @@ describe("build_file_entries with comment_counts", function()
end)
end)

describe("count_viewed", function()
it("counts all VIEWED files", function()
local viewed = { ["a.lua"] = "VIEWED", ["b.lua"] = "VIEWED", ["c.lua"] = "VIEWED" }
local changed = { { path = "a.lua" }, { path = "b.lua" }, { path = "c.lua" } }
assert.are.equal(3, files.count_viewed(viewed, changed))
end)

it("counts only VIEWED, not UNVIEWED or DISMISSED", function()
local viewed = { ["a.lua"] = "VIEWED", ["b.lua"] = "UNVIEWED", ["c.lua"] = "DISMISSED" }
local changed = { { path = "a.lua" }, { path = "b.lua" }, { path = "c.lua" } }
assert.are.equal(1, files.count_viewed(viewed, changed))
end)

it("returns 0 when no files are VIEWED", function()
local viewed = { ["a.lua"] = "UNVIEWED", ["b.lua"] = "UNVIEWED" }
local changed = { { path = "a.lua" }, { path = "b.lua" } }
assert.are.equal(0, files.count_viewed(viewed, changed))
end)

it("returns 0 for empty changed_files", function()
local viewed = { ["a.lua"] = "VIEWED" }
assert.are.equal(0, files.count_viewed(viewed, {}))
end)

it("returns 0 when viewed_files is nil", function()
local changed = { { path = "a.lua" }, { path = "b.lua" } }
assert.are.equal(0, files.count_viewed(nil, changed))
end)

it("ignores paths in viewed_files not in changed_files", function()
local viewed = { ["a.lua"] = "VIEWED", ["x.lua"] = "VIEWED" }
local changed = { { path = "a.lua" }, { path = "b.lua" } }
assert.are.equal(1, files.count_viewed(viewed, changed))
end)
end)

describe("apply_viewed_toggle", function()
local config = require("fude.config")
local helpers = require("tests.helpers")
Expand Down
Loading
Loading