Skip to content

Commit

Permalink
refactor!: render the context preview lazily, not immediately
Browse files Browse the repository at this point in the history
This should not be a breaking change, but please report any issues.

The motivation for this change is:
- I started to add a backend for git grep, but it does not support
  getting a context for matches like ripgrep does.
- In many cases, it is wasteful to render the context preview for every
  match anyway, because there might be 50 matches, and the user might
  look at only 0-3 of them
- The performance on my machine with this style was good
  • Loading branch information
mikavilpas committed Feb 22, 2025
1 parent db73596 commit 9d1351f
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 143 deletions.
8 changes: 1 addition & 7 deletions lua/blink-ripgrep/backends/ripgrep/ripgrep.lua
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ function RipgrepBackend:get_matches(prefix, context, resolve)
local parsed =
require("blink-ripgrep.backends.ripgrep.ripgrep_parser").parse(
lines,
cwd,
self.config.context_size
cwd
)
local kinds = require("blink.cmp.types").CompletionItemKind

Expand All @@ -73,10 +72,6 @@ function RipgrepBackend:get_matches(prefix, context, resolve)
-- way to display the same match multiple times
if not items[matchkey] then
local label = match.match.text
local docstring = ""
for _, line in ipairs(match.context_preview) do
docstring = docstring .. line.text .. "\n"
end

local draw_docs = function(draw_opts)
require("blink-ripgrep.documentation").render_item_documentation(
Expand All @@ -91,7 +86,6 @@ function RipgrepBackend:get_matches(prefix, context, resolve)
items[matchkey] = {
documentation = {
kind = "markdown",
value = docstring,
draw = draw_docs,
-- legacy, will be removed in a future release of blink
-- https://github.com/Saghen/blink.cmp/issues/1113
Expand Down
51 changes: 1 addition & 50 deletions lua/blink-ripgrep/backends/ripgrep/ripgrep_parser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ local M = {}

---@class blink-ripgrep.RipgrepFile
---@field language string the treesitter language of the file, used to determine what grammar to highlight the preview with
---@field lines table<number, string> the context preview, shared for all the matches in this file so that they can display a subset
---@field matches table<string,blink-ripgrep.RipgrepMatch>
---@field relative_to_cwd string the relative path of the file to the current working directory

Expand All @@ -14,7 +13,6 @@ local M = {}
---@field start_col number
---@field end_col number
---@field match {text: string} the matched text
---@field context_preview blink-ripgrep.NumberedLine[] the preview of this match. Each key is the line number in the original file, and each value is the line of context (text)

---@param json unknown
---@param output blink-ripgrep.RipgrepOutput
Expand All @@ -23,8 +21,6 @@ local function get_file_context(json, output)
local filename = json.data.path.text
local file = output.files[filename]
local line_number = json.data.line_number
-- assert(line_number)
-- assert(not file.lines[line_number])

return file, line_number
end
Expand All @@ -36,8 +32,7 @@ end
--
---@param ripgrep_output string[] ripgrep output in jsonl format
---@param cwd string the current working directory
---@param context_size number the number of lines of context to include in the output
function M.parse(ripgrep_output, cwd, context_size)
function M.parse(ripgrep_output, cwd)
---@type blink-ripgrep.RipgrepOutput
local output = { files = {} }

Expand All @@ -63,16 +58,11 @@ function M.parse(ripgrep_output, cwd, context_size)

output.files[filename] = {
language = language,
lines = {},
matches = {},
relative_to_cwd = relative_filename,
}
elseif json.type == "context" then
local file, line_number = get_file_context(json, output)
file.lines[line_number] = json.data.lines.text
elseif json.type == "match" then
local file, line_number = get_file_context(json, output)
file.lines[line_number] = json.data.lines.text

local text = json.data.submatches[1].match.text

Expand All @@ -82,51 +72,12 @@ function M.parse(ripgrep_output, cwd, context_size)
end_col = json.data.submatches[1]["end"],
match = { text = text },
line_number = line_number,
context_preview = {},
}
end
elseif json.type == "end" then
-- Right now, we have collected the necessary lines for the context in
-- previous steps. Get the context preview for each match.
local filename = json.data.path.text
local file = output.files[filename]

for _, match in pairs(file.matches) do
match.context_preview =
M.get_context_preview(file.lines, match.line_number, context_size)
end

-- clear the lines to save memory
file.lines = {}
end
end
end
return output
end

---@alias blink-ripgrep.NumberedLine {line_number: number, text: string}

---@param lines table<number, string>
---@param matched_line number the line number the match was found on
---@param context_size number how many lines of context to include before and after the match
---@return blink-ripgrep.NumberedLine[]
function M.get_context_preview(lines, matched_line, context_size)
---@type blink-ripgrep.NumberedLine[]
local context_preview = {}

local start_line = math.max(1, matched_line - context_size)
local end_line = matched_line + context_size

for i = start_line, end_line do
local line = lines[i]

if line then
local data = { line_number = i, text = line:gsub("%s*$", "") }
context_preview[#context_preview + 1] = data
end
end

return context_preview
end

return M
40 changes: 37 additions & 3 deletions lua/blink-ripgrep/documentation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pcall(function()
end)
vim.api.nvim_set_hl(0, "BlinkRipgrepMatch", { link = "Search", default = true })

---@alias blink-ripgrep.NumberedLine {line_number: number, text: string}

---@param config blink-ripgrep.Options
---@param draw_opts blink.cmp.CompletionDocumentationDrawOpts
---@param file blink-ripgrep.RipgrepFile
Expand All @@ -23,8 +25,14 @@ function documentation.render_item_documentation(config, draw_opts, file, match)
- 1
),
}
for _, data in ipairs(match.context_preview) do
table.insert(text, data.text)

local context_preview = documentation.get_match_context(
config.context_size,
match.line_number,
file.relative_to_cwd
)
for _, line in ipairs(context_preview) do
table.insert(text, line.text)
end

-- TODO add extmark highlighting for the divider line like in blink
Expand Down Expand Up @@ -63,8 +71,34 @@ function documentation.render_item_documentation(config, draw_opts, file, match)
require("blink-ripgrep.highlighting").highlight_match_in_doc_window(
bufnr,
match,
highlight_ns_id
highlight_ns_id,
context_preview
)
end

---@param context_size number
---@param match_line number # the line number the match was found on
---@param file_path string
function documentation.get_match_context(context_size, match_line, file_path)
local start_line = math.max(1, match_line - context_size)
local end_line = match_line + context_size

local text = vim.fn.readfile(file_path, "", end_line)
assert(type(text) == "table", "expected table from readfile")
---@cast text string[]

---@type blink-ripgrep.NumberedLine[]
local context = {}
for i, line in ipairs(text) do
if i >= start_line then
table.insert(context, {
line_number = i,
text = line,
} --[[@as blink-ripgrep.NumberedLine]])
end
end

return context
end

return documentation
11 changes: 9 additions & 2 deletions lua/blink-ripgrep/highlighting.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ local M = {}
---@param bufnr number
---@param match blink-ripgrep.RipgrepMatch
---@param highlight_ns_id number
function M.highlight_match_in_doc_window(bufnr, match, highlight_ns_id)
---@param context_preview blink-ripgrep.NumberedLine[]
function M.highlight_match_in_doc_window(
bufnr,
match,
highlight_ns_id,
context_preview
)
---@type number | nil
local line_in_docs = nil
for line, data in ipairs(match.context_preview) do
for line, data in ipairs(context_preview) do
if data.line_number == match.line_number then
line_in_docs = line
break
Expand All @@ -18,6 +24,7 @@ function M.highlight_match_in_doc_window(bufnr, match, highlight_ns_id)

assert(line_in_docs, "missing line in docs")

-- highlight the word that matched in this context preview
vim.api.nvim_buf_set_extmark(
bufnr,
highlight_ns_id,
Expand Down
109 changes: 109 additions & 0 deletions spec/blink-ripgrep/documentation_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
local documentation = require("blink-ripgrep.documentation")
local assert = require("luassert")

---@param lines string[]
local function create_test_file(lines)
local target_file_path = vim.fn.tempname()
local file = io.open(target_file_path, "w") -- Open or create the file in write mode
assert(file, "Failed to create file " .. target_file_path)
if file then
for _, line in ipairs(lines) do
file:write(line .. "\n")
end
file:close()
end
local stat = vim.uv.fs_stat(target_file_path)
assert(stat)
assert(stat.type == "file")

return target_file_path
end

describe("get_context_preview", function()
it("can display context around the match", function()
-- the happy path case
local lines = {
"line 1",
"line 2",
"line 3",
"line 4",
"line 5",
"line 6",
"line 7",
"line 8",
"line 9",
"line 10",
}
local file = create_test_file(lines)

local matched_line = 4
local context_size = 1
local result =
documentation.get_match_context(context_size, matched_line, file)

assert.same(result, {
{ line_number = 3, text = "line 3" },
{ line_number = 4, text = "line 4" },
{ line_number = 5, text = "line 5" },
})
end)

it("does not crash if context_size is too large", function()
local lines = {
"line 1",
}
local file = create_test_file(lines)

local matched_line = 1
local context_size = 10
local result =
documentation.get_match_context(context_size, matched_line, file)

assert.same(result, {
{ line_number = 1, text = "line 1" },
})
end)

it("does not crash if context_size is too small", function()
local lines = {
"line 1",
}
local file = create_test_file(lines)

local matched_line = 1
local context_size = 0
local result =
documentation.get_match_context(context_size, matched_line, file)

assert.same(result, {
{ line_number = 1, text = "line 1" },
})
end)

it("can display context around the match at the end of the file", function()
local lines = {
"line 1",
"line 2",
"line 3",
"line 4",
"line 5",
"line 6",
"line 7",
"line 8",
"line 9",
"line 10",
}
local file = create_test_file(lines)

local matched_line = 9
local context_size = 1
local result =
documentation.get_match_context(context_size, matched_line, file)

assert.same(result, {
{ line_number = 8, text = "line 8" },
{ line_number = 9, text = "line 9" },
{ line_number = 10, text = "line 10" },
})
end)
end)
Loading

0 comments on commit 9d1351f

Please sign in to comment.