From d6f63e1cb990b84b97f5cd6bedccb9295c989698 Mon Sep 17 00:00:00 2001 From: Robin Gruyters <2082795+rgruyters@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:19:57 +0200 Subject: [PATCH 1/3] fix(nvim): keep grep match bg on preview target line The preview target line used an extmark with line_hl_group = 'CursorLine'. A line_hl_group background overrides every hl_group background on that line regardless of priority, so grep matches (IncSearch / hl.grep_match) lost their background there. In fuzzy mode that is the only highlighted line. Emulate the cursor line with a low priority full-width range highlight instead so the match highlight wins. --- lua/fff/location_utils.lua | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/lua/fff/location_utils.lua b/lua/fff/location_utils.lua index aa0dcbd73..889b59d95 100644 --- a/lua/fff/location_utils.lua +++ b/lua/fff/location_utils.lua @@ -1,5 +1,7 @@ local M = {} +local set_cursor_line_mark + --- Jump to a location in the current buffer --- @param location table|nil Location data from search results function M.jump_to_location(location) @@ -51,11 +53,12 @@ function M.highlight_location(bufnr, location, namespace) local line_content = vim.api.nvim_buf_get_lines(bufnr, target_line - 1, target_line, false)[1] or '' local end_col = math.min(target_col + 1, #line_content) + local line_ok, line_mark_id = set_cursor_line_mark(bufnr, namespace, target_line - 1) + if line_ok then table.insert(extmarks, { id = line_mark_id, line = target_line - 1 }) end + local ok, mark_id = pcall(vim.api.nvim_buf_set_extmark, bufnr, namespace, target_line - 1, target_col, { end_col = end_col, hl_group = 'IncSearch', -- inc search are better visible for a single chars - line_hl_group = 'CursorLine', - number_hl_group = 'CursorLineNr', priority = 1000, }) @@ -82,11 +85,12 @@ function M.highlight_location(bufnr, location, namespace) local line_content = vim.api.nvim_buf_get_lines(bufnr, start_line - 1, start_line, false)[1] or '' end_col = math.min(end_col, #line_content) + local line_ok, line_mark_id = set_cursor_line_mark(bufnr, namespace, start_line - 1) + if line_ok then table.insert(extmarks, { id = line_mark_id, line = start_line - 1 }) end + local ok, mark_id = pcall(vim.api.nvim_buf_set_extmark, bufnr, namespace, start_line - 1, start_col, { end_col = end_col, hl_group = 'IncSearch', - line_hl_group = 'CursorLine', - number_hl_group = 'CursorLineNr', priority = 1000, }) @@ -162,11 +166,7 @@ function M.highlight_grep_matches(bufnr, location, namespace) -- line stays styled until it scrolls out of view. if location.line then local target_line = math.max(1, math.min(location.line, line_count)) - local ok, mark_id = pcall(vim.api.nvim_buf_set_extmark, bufnr, namespace, target_line - 1, 0, { - line_hl_group = 'CursorLine', - number_hl_group = 'CursorLineNr', - priority = 999, - }) + local ok, mark_id = set_cursor_line_mark(bufnr, namespace, target_line - 1) if ok then table.insert(extmarks, { id = mark_id, line = target_line - 1 }) end end @@ -305,4 +305,17 @@ function M.format_location(location) return '' end +-- `line_hl_group` bg overrides every `hl_group` bg on the line regardless of +-- priority, so emulate it with a low priority full-width range highlight. +set_cursor_line_mark = function(bufnr, namespace, row) + return pcall(vim.api.nvim_buf_set_extmark, bufnr, namespace, row, 0, { + end_row = row + 1, + end_col = 0, + hl_eol = true, + hl_group = 'CursorLine', + number_hl_group = 'CursorLineNr', + priority = 50, + }) +end + return M From eae1af7f4f072d38da1ad26b87622fbbf3f9a355 Mon Sep 17 00:00:00 2001 From: Robin Gruyters <2082795+rgruyters@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:31:49 +0200 Subject: [PATCH 2/3] fix(nvim): pin preview cursor line above syntax highlights Raise the cursor line range priority to 999 so syntax backgrounds can't fragment it while matches (1000) still win. Add specs for the three highlight paths. --- lua/fff/location_utils.lua | 4 +-- tests/location_utils_spec.lua | 68 +++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/location_utils_spec.lua diff --git a/lua/fff/location_utils.lua b/lua/fff/location_utils.lua index 889b59d95..81e134288 100644 --- a/lua/fff/location_utils.lua +++ b/lua/fff/location_utils.lua @@ -306,7 +306,7 @@ function M.format_location(location) end -- `line_hl_group` bg overrides every `hl_group` bg on the line regardless of --- priority, so emulate it with a low priority full-width range highlight. +-- priority, so emulate it with a full-width range above syntax but below matches. set_cursor_line_mark = function(bufnr, namespace, row) return pcall(vim.api.nvim_buf_set_extmark, bufnr, namespace, row, 0, { end_row = row + 1, @@ -314,7 +314,7 @@ set_cursor_line_mark = function(bufnr, namespace, row) hl_eol = true, hl_group = 'CursorLine', number_hl_group = 'CursorLineNr', - priority = 50, + priority = 999, }) end diff --git a/tests/location_utils_spec.lua b/tests/location_utils_spec.lua new file mode 100644 index 000000000..6b9a50656 --- /dev/null +++ b/tests/location_utils_spec.lua @@ -0,0 +1,68 @@ +---@diagnostic disable: undefined-field, need-check-nil +local location_utils = require('fff.location_utils') + +local function marks_by_group(buf, ns) + local by_group = {} + for _, mark in ipairs(vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true })) do + local details = mark[4] + assert.is_nil(details.line_hl_group) + if details.hl_group then + by_group[details.hl_group] = by_group[details.hl_group] or {} + table.insert(by_group[details.hl_group], { row = mark[2], col = mark[3], details = details }) + end + end + return by_group +end + +local function assert_cursor_line_below(marks, match_group, row) + local cursor_line = marks.CursorLine[1] + assert.are.equal(1, #marks.CursorLine) + assert.are.equal(row, cursor_line.row) + assert.are.equal(0, cursor_line.col) + assert.are.equal(row + 1, cursor_line.details.end_row) + assert.is_true(cursor_line.details.hl_eol) + assert.are.equal('CursorLineNr', cursor_line.details.number_hl_group) + + for _, match in ipairs(marks[match_group]) do + assert.is_true(match.details.priority > cursor_line.details.priority) + end +end + +describe('location_utils highlight', function() + local buf, ns + + before_each(function() + buf = vim.api.nvim_create_buf(false, true) + ns = vim.api.nvim_create_namespace('fff_location_utils_spec') + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { 'hello world', 'abc hello x' }) + end) + + after_each(function() vim.api.nvim_buf_delete(buf, { force = true }) end) + + it('keeps fuzzy grep matches above the pinned cursor line', function() + location_utils.highlight_location(buf, { grep_query = 'hlo', line = 2, fuzzy_match_ranges = { { 4, 9 } } }, ns) + local marks = marks_by_group(buf, ns) + local grep_hl = require('fff.conf').get().hl.grep_match or 'IncSearch' + + assert.are.equal(1, #marks[grep_hl]) + assert.are.equal(4, marks[grep_hl][1].col) + assert_cursor_line_below(marks, grep_hl, 1) + end) + + it('keeps line:col highlight above the cursor line', function() + location_utils.highlight_location(buf, { line = 2, col = 5 }, ns) + local marks = marks_by_group(buf, ns) + + assert.are.equal(4, marks.IncSearch[1].col) + assert_cursor_line_below(marks, 'IncSearch', 1) + end) + + it('keeps single line range highlight above the cursor line', function() + location_utils.highlight_location(buf, { start = { line = 1, col = 1 }, ['end'] = { line = 1, col = 6 } }, ns) + local marks = marks_by_group(buf, ns) + + assert.are.equal(0, marks.IncSearch[1].col) + assert.are.equal(5, marks.IncSearch[1].details.end_col) + assert_cursor_line_below(marks, 'IncSearch', 0) + end) +end) From e4db165ad42d9b7e30412f3caae16bd82306d7dc Mon Sep 17 00:00:00 2001 From: Robin Gruyters <2082795+rgruyters@users.noreply.github.com> Date: Fri, 25 Sep 2026 08:45:15 +0200 Subject: [PATCH 3/3] docs(nvim): add docstrings to cursor line helper and spec helpers --- lua/fff/location_utils.lua | 8 ++++++-- tests/location_utils_spec.lua | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lua/fff/location_utils.lua b/lua/fff/location_utils.lua index 81e134288..1f74ce174 100644 --- a/lua/fff/location_utils.lua +++ b/lua/fff/location_utils.lua @@ -305,8 +305,12 @@ function M.format_location(location) return '' end --- `line_hl_group` bg overrides every `hl_group` bg on the line regardless of --- priority, so emulate it with a full-width range above syntax but below matches. +--- Pin a row with a full-width CursorLine range, above syntax but below matches +--- (`line_hl_group` bg would override every `hl_group` bg regardless of priority) +--- @param bufnr number Buffer number +--- @param namespace number Namespace for extmarks +--- @param row number 0-based row to highlight +--- @return boolean ok, number|string mark_id_or_err pcall result of nvim_buf_set_extmark set_cursor_line_mark = function(bufnr, namespace, row) return pcall(vim.api.nvim_buf_set_extmark, bufnr, namespace, row, 0, { end_row = row + 1, diff --git a/tests/location_utils_spec.lua b/tests/location_utils_spec.lua index 6b9a50656..7f7858d3f 100644 --- a/tests/location_utils_spec.lua +++ b/tests/location_utils_spec.lua @@ -1,6 +1,10 @@ ---@diagnostic disable: undefined-field, need-check-nil local location_utils = require('fff.location_utils') +--- Collect extmarks grouped by hl_group, asserting none use line_hl_group +--- @param buf number Buffer number +--- @param ns number Namespace to read extmarks from +--- @return table marks Marks keyed by hl_group with row, col and details local function marks_by_group(buf, ns) local by_group = {} for _, mark in ipairs(vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true })) do @@ -14,6 +18,10 @@ local function marks_by_group(buf, ns) return by_group end +--- Assert a single full-width CursorLine mark on row, with lower priority than every match +--- @param marks table Result of marks_by_group +--- @param match_group string Highlight group of the match marks +--- @param row number 0-based row expected to carry the CursorLine mark local function assert_cursor_line_below(marks, match_group, row) local cursor_line = marks.CursorLine[1] assert.are.equal(1, #marks.CursorLine)