diff --git a/lua/fff/location_utils.lua b/lua/fff/location_utils.lua index aa0dcbd73..1f74ce174 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,21 @@ function M.format_location(location) return '' end +--- 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, + end_col = 0, + hl_eol = true, + hl_group = 'CursorLine', + number_hl_group = 'CursorLineNr', + priority = 999, + }) +end + return M diff --git a/tests/location_utils_spec.lua b/tests/location_utils_spec.lua new file mode 100644 index 000000000..7f7858d3f --- /dev/null +++ b/tests/location_utils_spec.lua @@ -0,0 +1,76 @@ +---@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 + 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 + +--- 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) + 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)