chore: import upstream snapshot with attribution
e2e Tests / e2e (macos-latest) (push) Waiting to run
Nix CI / check (push) Waiting to run
Python CI / Python bindings (macos-latest) (push) Waiting to run
e2e Tests / e2e (windows-latest) (push) Waiting to run
Python CI / Python bindings (windows-latest) (push) Waiting to run
Build & Publish / Build Neovim aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Neovim x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build MCP aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build MCP x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Waiting to run
Build & Publish / Release (push) Blocked by required conditions
Build & Publish / Publish Python wheels to PyPI (push) Blocked by required conditions
Build & Publish / Publish Rust crates (push) Blocked by required conditions
Build & Publish / Publish npm packages (push) Blocked by required conditions
Rust CI / Fuzz Tests (windows-latest) (push) Waiting to run
Rust CI / Test (macos-latest) (push) Waiting to run
Rust CI / Test (windows-latest) (push) Waiting to run
Rust CI / Fuzz Tests (macos-latest) (push) Waiting to run
Python CI / Python bindings (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Neovim aarch64-linux-android (push) Failing after 0s
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python sdist (push) Failing after 1s
Rust CI / Fuzz Tests (ubuntu-latest) (push) Failing after 1s
Rust CI / Build i686-unknown-linux-gnu (push) Failing after 0s
Rust CI / cargo clippy (push) Failing after 1s
e2e Tests / e2e (ubuntu-latest) (push) Failing after 1s
Lua CI / luacheck lint (push) Failing after 1s
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI aarch64-linux-android (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Failing after 1s
Spelling / Spell Check with Typos (push) Failing after 0s
Rust CI / Test (ubuntu-latest) (push) Failing after 0s
Rust CI / cargo fmt (push) Failing after 0s
Stylua / Check lua files using Stylua (push) Failing after 1s
Lua CI / lua-language-server type check (push) Failing after 12m29s
e2e Tests / e2e (alpine-musl) (push) Successful in 57m31s
e2e Tests / e2e (macos-latest) (push) Waiting to run
Nix CI / check (push) Waiting to run
Python CI / Python bindings (macos-latest) (push) Waiting to run
e2e Tests / e2e (windows-latest) (push) Waiting to run
Python CI / Python bindings (windows-latest) (push) Waiting to run
Build & Publish / Build Neovim aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Neovim x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build MCP aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build MCP x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Waiting to run
Build & Publish / Release (push) Blocked by required conditions
Build & Publish / Publish Python wheels to PyPI (push) Blocked by required conditions
Build & Publish / Publish Rust crates (push) Blocked by required conditions
Build & Publish / Publish npm packages (push) Blocked by required conditions
Rust CI / Fuzz Tests (windows-latest) (push) Waiting to run
Rust CI / Test (macos-latest) (push) Waiting to run
Rust CI / Test (windows-latest) (push) Waiting to run
Rust CI / Fuzz Tests (macos-latest) (push) Waiting to run
Python CI / Python bindings (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Neovim aarch64-linux-android (push) Failing after 0s
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python sdist (push) Failing after 1s
Rust CI / Fuzz Tests (ubuntu-latest) (push) Failing after 1s
Rust CI / Build i686-unknown-linux-gnu (push) Failing after 0s
Rust CI / cargo clippy (push) Failing after 1s
e2e Tests / e2e (ubuntu-latest) (push) Failing after 1s
Lua CI / luacheck lint (push) Failing after 1s
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI aarch64-linux-android (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Failing after 1s
Spelling / Spell Check with Typos (push) Failing after 0s
Rust CI / Test (ubuntu-latest) (push) Failing after 0s
Rust CI / cargo fmt (push) Failing after 0s
Stylua / Check lua files using Stylua (push) Failing after 1s
Lua CI / lua-language-server type check (push) Failing after 12m29s
e2e Tests / e2e (alpine-musl) (push) Successful in 57m31s
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
local M = {}
|
||||
|
||||
--- Jump to a location in the current buffer
|
||||
--- @param location table|nil Location data from search results
|
||||
function M.jump_to_location(location)
|
||||
if not location then return end
|
||||
|
||||
local current_buf = vim.api.nvim_get_current_buf()
|
||||
local line_count = vim.api.nvim_buf_line_count(current_buf)
|
||||
|
||||
if location.line then
|
||||
local target_line = math.max(1, math.min(location.line, line_count))
|
||||
local target_col = location.col and math.max(0, location.col - 1) or 0
|
||||
|
||||
vim.api.nvim_win_set_cursor(0, { target_line, target_col })
|
||||
vim.cmd('normal! zz')
|
||||
elseif location.start and location['end'] then
|
||||
-- Extract line numbers from nested structure
|
||||
local start_line = math.max(1, math.min(location.start.line, line_count))
|
||||
local end_line = math.max(start_line, math.min(location['end'].line, line_count))
|
||||
|
||||
-- start in the visual mode and selecting the range backwards so the cursor ends up at the start
|
||||
vim.api.nvim_win_set_cursor(0, { end_line, 0 })
|
||||
vim.cmd('normal! V')
|
||||
if end_line > start_line then vim.cmd('normal! ' .. (end_line - start_line) .. 'k') end
|
||||
vim.cmd('normal! zz')
|
||||
end
|
||||
end
|
||||
|
||||
--- Highlight a location range in a buffer using extmarks
|
||||
--- @param bufnr number Buffer number
|
||||
--- @param location table|nil Location data from search results
|
||||
--- @param namespace number Namespace for extmarks
|
||||
--- @return table|nil Highlight extmark details for cleanup
|
||||
function M.highlight_location(bufnr, location, namespace)
|
||||
if not location or not vim.api.nvim_buf_is_valid(bufnr) then return nil end
|
||||
|
||||
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
||||
local extmarks = {}
|
||||
|
||||
-- Grep mode: highlight all occurrences of the search pattern across visible lines
|
||||
if location.grep_query and location.grep_query ~= '' then
|
||||
return M.highlight_grep_matches(bufnr, location, namespace)
|
||||
end
|
||||
|
||||
if location.line then
|
||||
local target_line = math.max(1, math.min(location.line, line_count))
|
||||
|
||||
if location.col then
|
||||
local target_col = math.max(0, location.col - 1)
|
||||
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 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,
|
||||
})
|
||||
|
||||
if ok then table.insert(extmarks, { id = mark_id, line = target_line - 1 }) end
|
||||
else
|
||||
local ok, mark_id = pcall(vim.api.nvim_buf_set_extmark, bufnr, namespace, target_line - 1, 0, {
|
||||
line_hl_group = 'Visual',
|
||||
number_hl_group = 'CursorLineNr',
|
||||
priority = 1000,
|
||||
})
|
||||
|
||||
if ok then table.insert(extmarks, { id = mark_id, line = target_line - 1 }) end
|
||||
end
|
||||
elseif location.start and location['end'] then
|
||||
local start_line = math.max(1, math.min(location.start.line, line_count))
|
||||
local end_line = math.max(start_line, math.min(location['end'].line, line_count))
|
||||
|
||||
-- Check if we have column information for exact range highlighting
|
||||
if location.start.col and location['end'].col then
|
||||
if start_line == end_line then
|
||||
-- Single line range with columns: highlight exact character range
|
||||
local start_col = math.max(0, location.start.col - 1)
|
||||
local end_col = location['end'].col - 1
|
||||
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 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,
|
||||
})
|
||||
|
||||
if ok then table.insert(extmarks, { id = mark_id, line = start_line - 1 }) end
|
||||
else
|
||||
-- Multi-line range with exact columns: highlight precise ranges
|
||||
for line = start_line, end_line do
|
||||
local line_start_col, line_end_col
|
||||
|
||||
if line == start_line then
|
||||
-- First line: from start_col to end of line
|
||||
line_start_col = math.max(0, location.start.col - 1)
|
||||
local line_content = vim.api.nvim_buf_get_lines(bufnr, line - 1, line, false)[1] or ''
|
||||
line_end_col = #line_content
|
||||
elseif line == end_line then
|
||||
-- Last line: from beginning to end_col
|
||||
line_start_col = 0
|
||||
line_end_col = location['end'].col - 1
|
||||
local line_content = vim.api.nvim_buf_get_lines(bufnr, line - 1, line, false)[1] or ''
|
||||
line_end_col = math.min(line_end_col, #line_content)
|
||||
else
|
||||
-- Middle lines: entire line
|
||||
line_start_col = 0
|
||||
local line_content = vim.api.nvim_buf_get_lines(bufnr, line - 1, line, false)[1] or ''
|
||||
line_end_col = #line_content
|
||||
end
|
||||
|
||||
local ok, mark_id = pcall(vim.api.nvim_buf_set_extmark, bufnr, namespace, line - 1, line_start_col, {
|
||||
end_col = line_end_col,
|
||||
hl_group = 'Visual',
|
||||
priority = 1000,
|
||||
})
|
||||
|
||||
if ok then table.insert(extmarks, { id = mark_id, line = line - 1 }) end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Multi-line or no columns: highlight entire lines
|
||||
for line = start_line, end_line do
|
||||
local ok, mark_id = pcall(vim.api.nvim_buf_set_extmark, bufnr, namespace, line - 1, 0, {
|
||||
line_hl_group = 'Visual',
|
||||
priority = 1000,
|
||||
})
|
||||
|
||||
if ok then table.insert(extmarks, { id = mark_id, line = line - 1 }) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return #extmarks > 0 and extmarks or nil
|
||||
end
|
||||
|
||||
--- Highlight all occurrences of a grep pattern in the preview buffer.
|
||||
--- For plain text and regex modes: highlights every match on all loaded lines
|
||||
--- using Lua string.find with the query text.
|
||||
--- For fuzzy mode: uses the pre-computed match byte offsets from Rust on the
|
||||
--- target line only, since the fuzzy needle (e.g. "shcema") won't match via
|
||||
--- literal search against the actual content (e.g. "schema").
|
||||
--- @param bufnr number Buffer number
|
||||
--- @param location table Location with .grep_query, .line, optional .col, optional .fuzzy_match_ranges
|
||||
--- @param namespace number Namespace for extmarks
|
||||
--- @return table|nil Highlight extmark details for cleanup
|
||||
function M.highlight_grep_matches(bufnr, location, namespace)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then return nil end
|
||||
|
||||
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
||||
local extmarks = {}
|
||||
local grep_hl = require('fff.conf').get().hl.grep_match or 'IncSearch'
|
||||
|
||||
-- Pin CursorLine + CursorLineNr to the target match line via extmark, so
|
||||
-- the highlight stays anchored when the user pages the preview viewport
|
||||
-- with <C-d>/<C-u>. The cursor itself moves with paging, but the match
|
||||
-- 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,
|
||||
})
|
||||
if ok then table.insert(extmarks, { id = mark_id, line = target_line - 1 }) end
|
||||
end
|
||||
|
||||
-- Fuzzy mode: use pre-computed byte offsets from Rust's match_indices.
|
||||
-- These are the exact matched character positions within the line, already
|
||||
-- computed by the SIMD scoring + reference smith-waterman traceback.
|
||||
-- We only highlight the target line since each fuzzy result has its own
|
||||
-- unique set of matched positions.
|
||||
if location.fuzzy_match_ranges and location.line then
|
||||
local target_line = math.max(1, math.min(location.line, line_count))
|
||||
for _, range in ipairs(location.fuzzy_match_ranges) do
|
||||
local start_byte = range[1] -- 0-based byte offset
|
||||
local end_byte = range[2] -- 0-based exclusive end
|
||||
local ok, mark_id = pcall(vim.api.nvim_buf_set_extmark, bufnr, namespace, target_line - 1, start_byte, {
|
||||
end_col = end_byte,
|
||||
hl_group = grep_hl,
|
||||
priority = 1000,
|
||||
})
|
||||
if ok then table.insert(extmarks, { id = mark_id, line = target_line - 1 }) end
|
||||
end
|
||||
-- Fuzzy mode: only target line has matches; skip the plain-text scan below.
|
||||
return #extmarks > 0 and extmarks or nil
|
||||
end
|
||||
|
||||
local query = location.grep_query
|
||||
|
||||
-- Use the Rust GrepConfig parser as the single source of truth for
|
||||
-- stripping constraint tokens. This avoids duplicating constraint
|
||||
-- detection in Lua, which would break whenever a new token type is added.
|
||||
local fuzzy = require('fff.fuzzy')
|
||||
local parsed = fuzzy.parse_grep_query(query)
|
||||
local search_text = parsed.grep_text
|
||||
if search_text == '' then search_text = query end
|
||||
|
||||
if not search_text or search_text == '' then return nil end
|
||||
|
||||
-- Build case-insensitive pattern if the query has no uppercase (smart case)
|
||||
local has_upper = search_text:match('[A-Z]')
|
||||
|
||||
-- Highlight pattern occurrences in a window around the target line.
|
||||
-- Limit to ±200 lines from target to keep it fast for large files.
|
||||
local scan_start = 1
|
||||
local scan_end = line_count
|
||||
if location.line then
|
||||
scan_start = math.max(1, location.line - 200)
|
||||
scan_end = math.min(line_count, location.line + 200)
|
||||
end
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, scan_start - 1, scan_end, false)
|
||||
for idx, line in ipairs(lines) do
|
||||
local i = scan_start + idx - 1
|
||||
local search_line = has_upper and line or line:lower()
|
||||
local search_pat = has_upper and search_text or search_text:lower()
|
||||
local start_pos = 1
|
||||
while true do
|
||||
local s, e = search_line:find(search_pat, start_pos, true)
|
||||
if not s then break end
|
||||
-- s and e are 1-based byte positions; extmarks need 0-based
|
||||
local ok, mark_id = pcall(vim.api.nvim_buf_set_extmark, bufnr, namespace, i - 1, s - 1, {
|
||||
end_col = e,
|
||||
hl_group = grep_hl,
|
||||
priority = 1000,
|
||||
})
|
||||
if ok then table.insert(extmarks, { id = mark_id, line = i - 1 }) end
|
||||
start_pos = e + 1
|
||||
end
|
||||
end
|
||||
|
||||
return #extmarks > 0 and extmarks or nil
|
||||
end
|
||||
|
||||
--- Clear location highlights from a buffer
|
||||
--- @param bufnr number Buffer number
|
||||
--- @param namespace number Namespace for extmarks
|
||||
function M.clear_location_highlights(bufnr, namespace)
|
||||
if vim.api.nvim_buf_is_valid(bufnr) then vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) end
|
||||
end
|
||||
|
||||
--- Get the target line for scrolling preview to location
|
||||
--- @param location table|nil Location data from search results
|
||||
--- @return number|nil Target line number (1-indexed) for scrolling
|
||||
function M.get_target_line(location)
|
||||
if not location then return nil end
|
||||
|
||||
if location.line then
|
||||
return location.line
|
||||
elseif location.start then
|
||||
return location.start.line
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Check if location is valid for a buffer
|
||||
--- @param location table|nil Location data
|
||||
--- @param bufnr number Buffer number
|
||||
--- @return boolean True if location is valid for the buffer
|
||||
function M.is_valid_location(location, bufnr)
|
||||
if not location or not vim.api.nvim_buf_is_valid(bufnr) then return false end
|
||||
|
||||
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
||||
|
||||
if location.line then
|
||||
return location.line > 0 and location.line <= line_count
|
||||
elseif location.start and location['end'] then
|
||||
return location.start.line > 0 and location.start.line <= line_count
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Format location for display
|
||||
--- @param location table|nil Location data
|
||||
--- @return string Formatted location string
|
||||
function M.format_location(location)
|
||||
if not location then return '' end
|
||||
|
||||
if location.line and location.col then
|
||||
return string.format(':%d:%d', location.line, location.col)
|
||||
elseif location.line then
|
||||
return string.format(':%d', location.line)
|
||||
elseif location.start and location['end'] then
|
||||
-- Handle nested structure with optional column information
|
||||
if location.start.col and location['end'].col then
|
||||
return string.format(
|
||||
':%d:%d-%d:%d',
|
||||
location.start.line,
|
||||
location.start.col,
|
||||
location['end'].line,
|
||||
location['end'].col
|
||||
)
|
||||
else
|
||||
return string.format(':%d-%d', location.start.line, location['end'].line)
|
||||
end
|
||||
end
|
||||
|
||||
return ''
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user