Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ luacov.report.out

# Local review mode store (per-worktree review comments)
.fude/

# Neovim log written to cwd when the state dir is unwritable (e.g. headless test runs)
nvim.log
8 changes: 4 additions & 4 deletions CLAUDE.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,15 @@ require("fude").setup({
label = "[resolved]", -- Label string (comment browser / viewer / virtual text)
hl_group = "DiagnosticOk", -- Highlight group for resolved labels
},
-- Multi-line comment range display. A multi-line comment is shown on its
-- last line; inline boxes are titled with the range (e.g. "Comment L20-L29").
-- No background or sign column is used. Set a field to false to disable it.
comment_range = {
number_hl = "DiagnosticInfo", -- Line number highlight on the range (needs 'number' or 'relativenumber')
-- While the cursor is on the comment line:
cursor_number_hl = "DiagnosticWarn", -- replaces number_hl on the range
start_marker_hl = "DiagnosticHint", -- "↓ comment L20-L29" at the end of the first line
},
-- Side panel options
sidepanel = {
width = 40, -- Panel width in columns
Expand Down
35 changes: 35 additions & 0 deletions doc/fude.txt
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,15 @@ File opening position *fude-file-position*
|:FudeReviewToggleCommentStyle| are detected, the hint shows those
keybindings with `<leader>` notation. Otherwise, it shows the command
names. The hint disappears when the cursor moves to a non-comment line.

A multi-line comment is shown on its last line, so its first line is
marked separately: the inline box of the thread's head comment is
titled with the range (e.g. `Comment L20-L29`), the line numbers of the
range are highlighted with `comment_range.number_hl`, and while the
cursor is on the comment line the range's line numbers switch to
`comment_range.cursor_number_hl` and its first line gets a
`↓ comment L20-L29` marker at the end. None of these use a background
or the sign column, so diff colors and gitsigns stay visible.
Requires an active review session.

:FudeReviewToggleResolved *:FudeReviewToggleResolved*
Expand Down Expand Up @@ -1005,6 +1014,12 @@ Default configuration: >lua
label = "[resolved]", -- Label string
hl_group = "DiagnosticOk", -- Highlight group for resolved labels
},
-- Multi-line comment range display (false disables a field)
comment_range = {
number_hl = "DiagnosticInfo", -- Line number highlight on the range
cursor_number_hl = "DiagnosticWarn", -- Same, while on the comment line
start_marker_hl = "DiagnosticHint", -- First-line marker, while on the comment line
},
})
<

Expand Down Expand Up @@ -1270,6 +1285,26 @@ Options:
`resolved.hl_group` Highlight group for resolved labels
(default: "DiagnosticOk").

`comment_range.number_hl`
Highlight group for the line numbers of every line a
multi-line comment covers (default: "DiagnosticInfo").
Applied in both comment styles. Only the number column is
colored, so it takes no sign column slot and does not
override diff colors; it is invisible without 'number' or
'relativenumber'. Set to false to disable.

`comment_range.cursor_number_hl`
Highlight group that replaces `comment_range.number_hl` on
the range while the cursor is on the comment's line (its
last line); removed when the cursor leaves it
(default: "DiagnosticWarn"). Set to false to disable.

`comment_range.start_marker_hl`
Highlight group for the `↓ comment L20-L29` marker shown at
the end of the range's first line while the cursor is on
the comment's line (default: "DiagnosticHint"). Set to
false to disable.

`auto_reload.enabled`
Enable automatic periodic reload of review data
(default: false). When enabled, comments, changed files,
Expand Down
1 change: 1 addition & 0 deletions lua/fude/comments.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ M.build_review_comment_object = data.build_review_comment_object
M.merge_pending_into_comments = data.merge_pending_into_comments
M.pending_comments_to_array = data.pending_comments_to_array
M.get_comment_line_range = data.get_comment_line_range
M.get_multiline_ranges = data.get_multiline_ranges
M.get_reply_target_id = data.get_reply_target_id
M.build_submit_choices = data.build_submit_choices

Expand Down
19 changes: 19 additions & 0 deletions lua/fude/comments/data.lua
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,25 @@ function M.get_comment_line_range(comment)
return start_line, end_line
end

--- Collect the distinct line ranges of the multi-line comments in a list.
--- Single-line comments are skipped: their range is the line the comment is
--- shown on, so marking it adds nothing.
--- @param comments table[] comment objects
--- @return table[] ranges { start_line, end_line } in first-seen order
function M.get_multiline_ranges(comments)
local ranges = {}
local seen = {}
for _, comment in ipairs(comments) do
local start_line, end_line = M.get_comment_line_range(comment)
local key = start_line .. ":" .. end_line
if start_line < end_line and not seen[key] then
seen[key] = true
table.insert(ranges, { start_line = start_line, end_line = end_line })
end
end
return ranges
end

--- Get the reply target ID for a comment.
--- GitHub API doesn't allow replying to replies, so we need to find the top-level comment.
--- @param comment_id number the comment ID
Expand Down
10 changes: 10 additions & 0 deletions lua/fude/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ M.defaults = {
label = "[resolved]", -- Label string for resolved threads
hl_group = "DiagnosticOk", -- Highlight group for resolved labels
},
-- Show which lines a multi-line comment covers. None of these use a background
-- or the sign column, so diff colors and gitsigns stay visible. Set a field to
-- false to disable it.
comment_range = {
-- Line number highlight on every line of the range (needs 'number' or 'relativenumber').
number_hl = "DiagnosticInfo",
-- While the cursor is on the comment's line (the range's last line):
cursor_number_hl = "DiagnosticWarn", -- replaces number_hl on the range
start_marker_hl = "DiagnosticHint", -- "↓ comment L20-L29" at the end of the first line
},
keymaps = {
create_comment = "<leader>Rc",
view_comments = "<leader>Rv",
Expand Down
2 changes: 2 additions & 0 deletions lua/fude/ui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ local ref_ns = vim.api.nvim_create_namespace("fude_refs")

-- Re-export format functions (facade)
M.calculate_float_dimensions = format.calculate_float_dimensions
M.format_line_range = format.format_line_range
M.build_range_start_marker = format.build_range_start_marker
M.format_comments_for_display = format.format_comments_for_display
M.build_comments_float_title = format.build_comments_float_title
M.normalize_check = format.normalize_check
Expand Down
97 changes: 97 additions & 0 deletions lua/fude/ui/extmarks.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
local M = {}
local config = require("fude.config")
local data = require("fude.comments.data")
local drafts = require("fude.drafts")
local format = require("fude.ui.format")
local util = require("fude.util")

--- Get the namespace ID for flash/highlight extmarks.
Expand Down Expand Up @@ -77,6 +79,31 @@ function M.clear_comment_line_highlight()
comment_line_highlight.extmark_ids = {}
end

--- Highlight the line numbers of every line in the ranges, clamped to the
--- buffer (a local comment can point past EOF until it is marked outdated).
--- @param buf number buffer handle
--- @param ns number namespace
--- @param ranges table[] { start_line, end_line } (1-indexed, inclusive)
--- @param hl_group string highlight group for the number column
--- @param priority number extmark priority (a higher one wins on the same line)
--- @return number[] extmark ids
local function highlight_range_numbers(buf, ns, ranges, hl_group, priority)
local ids = {}
local line_count = vim.api.nvim_buf_line_count(buf)
for _, range in ipairs(ranges) do
for line = math.max(1, range.start_line), math.min(range.end_line, line_count) do
local ok, id = pcall(vim.api.nvim_buf_set_extmark, buf, ns, line - 1, 0, {
number_hl_group = hl_group,
priority = priority,
})
if ok then
table.insert(ids, id)
end
end
end
return ids
end

--- Render end-of-line virtualText indicators (comment count, pending, resolved)
--- for a set of comments on a line. Shared by the virtualText comment style and
--- by the inline style's fallback for resolved comments whose box is hidden by
Expand Down Expand Up @@ -142,6 +169,7 @@ function M.refresh_extmarks()
-- unrelated lines; clear instead.
if require("fude.local.session").in_commit_scope() then
vim.api.nvim_buf_clear_namespace(buf, state.ns_id, 0, -1)
M.rebuild_inline_hint(buf)
return
end

Expand All @@ -163,10 +191,17 @@ function M.refresh_extmarks()
if style == "inline" then
inline_opts = config.opts.inline or {}
end
local number_hl = (config.opts.comment_range or {}).number_hl

for _, line in ipairs(comment_lines) do
local comments = comments_mod.get_comments_at(rel_path, line)

-- Mark the line numbers a multi-line comment covers. Comments are drawn on
-- their last line only, so without this the first line is not visible.
if number_hl then
highlight_range_numbers(buf, state.ns_id, data.get_multiline_ranges(comments), number_hl, 40)
end

if style == "inline" then
-- Inline mode: display full comment content below the line.
-- When FudeReviewToggleResolved is off, resolved comments do not get an
Expand Down Expand Up @@ -235,6 +270,8 @@ function M.refresh_extmarks()
priority = 44,
})
end

M.rebuild_inline_hint(buf)
end

--- Clear all extmarks for a specific buffer.
Expand Down Expand Up @@ -293,6 +330,8 @@ local current_hint = {
buf = nil,
line = nil,
extmark_id = nil,
-- Range feedback (line numbers, start markers) for the comments on the hinted line
range_ids = {},
}

-- Cache for keymaps (avoid repeated keymap lookups on CursorMoved)
Expand All @@ -308,9 +347,51 @@ function M.clear_inline_hint()
if current_hint.buf and current_hint.extmark_id then
pcall(vim.api.nvim_buf_del_extmark, current_hint.buf, get_hint_ns(), current_hint.extmark_id)
end
if current_hint.buf then
for _, id in ipairs(current_hint.range_ids) do
pcall(vim.api.nvim_buf_del_extmark, current_hint.buf, get_hint_ns(), id)
end
end
current_hint.buf = nil
current_hint.line = nil
current_hint.extmark_id = nil
current_hint.range_ids = {}
end

--- Mark the multi-line ranges of the comments on the cursor line: emphasize
--- their line numbers and put a marker at the end of each range's first line.
--- A comment is drawn on the last line of its range, so this shows where it
--- starts. No background highlight is used, so diff colors stay visible.
--- @param buf number buffer handle
--- @param ns number hint namespace
--- @param comments table[] comments on the cursor line
--- @return number[] extmark ids
local function mark_cursor_ranges(buf, ns, comments)
local range_opts = config.opts.comment_range or {}
local ranges = data.get_multiline_ranges(comments)
local ids = {}
if range_opts.cursor_number_hl then
-- Above refresh_extmarks' number_hl (40) so it replaces it on these lines
ids = highlight_range_numbers(buf, ns, ranges, range_opts.cursor_number_hl, 45)
end
if range_opts.start_marker_hl then
local line_count = vim.api.nvim_buf_line_count(buf)
for _, range in ipairs(ranges) do
if range.start_line >= 1 and range.start_line <= line_count then
local ok, id = pcall(vim.api.nvim_buf_set_extmark, buf, ns, range.start_line - 1, 0, {
virt_text = {
{ " " .. format.build_range_start_marker(range.start_line, range.end_line), range_opts.start_marker_hl },
},
virt_text_pos = "eol",
priority = 200,
})
if ok then
table.insert(ids, id)
end
end
end
end
return ids
end

--- Convert internal keymap lhs to human-readable format.
Expand Down Expand Up @@ -457,11 +538,27 @@ function M.update_inline_hint()
current_hint.buf = buf
current_hint.line = cursor_line
current_hint.extmark_id = extmark_id
current_hint.range_ids = mark_cursor_ranges(buf, ns, comments)
Comment thread
Copilot marked this conversation as resolved.
end

-- Autocmd group for inline hint
local hint_augroup = nil

--- Rebuild the cursor-line hint and range feedback after a buffer's comments
--- were re-rendered. update_inline_hint skips a line it already marked, so a
--- reload or re-anchor would otherwise leave stale ranges until the cursor
--- moves. Only a hint already shown in `buf` is rebuilt: refresh_visible_extmarks
--- re-renders other windows through nvim_win_call, and rebuilding there would
--- move the hint out of the window the user is in.
--- @param buf number buffer whose comments were re-rendered
function M.rebuild_inline_hint(buf)
if current_hint.buf ~= buf then
return
end
M.clear_inline_hint()
M.update_inline_hint()
Comment on lines +555 to +559
end

--- Setup autocmd for inline hint updates.
function M.setup_inline_hint_autocmd()
if hint_augroup then
Expand Down
25 changes: 25 additions & 0 deletions lua/fude/ui/format.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,31 @@ function M.build_comment_separator(width)
return string.rep("-", math.max(1, math.floor(width)))
end

--- Format a multi-line comment range as "L20-L29".
--- @param start_line number|nil 1-indexed first line
--- @param end_line number|nil 1-indexed last line
--- @return string|nil label, nil for a single line or an invalid range
function M.format_line_range(start_line, end_line)
if type(start_line) ~= "number" or type(end_line) ~= "number" or start_line >= end_line then
return nil
end
return string.format("L%d-L%d", start_line, end_line)
end

--- Build the end-of-line marker shown on the first line of a multi-line
--- comment range while the cursor is on the comment ("↓ comment L20-L29").
--- The arrow points down because the comment is drawn on the range's last line.
--- @param start_line number 1-indexed first line
--- @param end_line number 1-indexed last line
--- @return string marker text ("" for a single line or an invalid range)
function M.build_range_start_marker(start_line, end_line)
local range = M.format_line_range(start_line, end_line)
if not range then
return ""
end
return "↓ comment " .. range
end

--- Build status badges for a comment header (" [agent]", " [approved]", ...).
--- Local review comments carry author_type ("human"|"agent"); plain GitHub
--- comments get "". A submitted review body (`review_state`, see
Expand Down
18 changes: 13 additions & 5 deletions lua/fude/ui/inline.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local M = {}

local data = require("fude.comments.data")
local format = require("fude.ui.format")
local util = require("fude.util")

Expand Down Expand Up @@ -83,12 +84,19 @@ function M.format_comments_for_inline(comments, format_date_fn, opts)
-- `is_resolved` is a thread-level state applied to every comment in the
-- thread, so the `[resolved thread]` label is shown only on the thread's
-- head comment (the oldest one, which has no in_reply_to_id) instead of
-- repeating on every reply box.
local label = " Comment "
-- repeating on every reply box. The line range of a multi-line comment
-- (` Comment L20-L29 `) is thread-level too, so it follows the same rule.
local is_head = util.is_null(comment.in_reply_to_id)
local title = "Comment"
local range = is_head and format.format_line_range(data.get_comment_line_range(comment))
if range then
title = title .. " " .. range
end
local label = " " .. title .. " "
if is_pending then
label = " Comment [pending] "
elseif comment.is_resolved and util.is_null(comment.in_reply_to_id) then
label = " Comment [resolved thread] "
label = " " .. title .. " [pending] "
elseif comment.is_resolved and is_head then
label = " " .. title .. " [resolved thread] "
end
local corner_width = 2 -- ╭ and ╮ are 1 cell each
local left_dash_width = 1 -- ─ after ╭
Expand Down
Loading
Loading