From ebc092759f4d38514391a198c648446e5b60883e Mon Sep 17 00:00:00 2001 From: Mika Vilpas Date: Fri, 21 Feb 2025 16:17:08 +0200 Subject: [PATCH] refactor: change the architecture to allow different search backends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right now this solves no issue, but I want to experiment with adding more search backends in the future. For this to be easier, it's probably good to have an idea of what a "backend" actually is 🙂 I think they can help solve performance issues in massive repositories. For example, I found out that "git grep" is much faster than ripgrep in some cases (5s vs 40s). Related to https://github.com/mikavilpas/blink-ripgrep.nvim/issues/110 --- .../backends/ripgrep/ripgrep.lua | 146 +++++++++++++ .../ripgrep}/ripgrep_command.lua | 0 .../{ => backends/ripgrep}/ripgrep_parser.lua | 0 lua/blink-ripgrep/documentation.lua | 70 ++++++ lua/blink-ripgrep/init.lua | 206 +----------------- spec/blink-ripgrep/get_command_spec.lua | 2 +- spec/blink-ripgrep/ripgrep_parser_spec.lua | 2 +- 7 files changed, 225 insertions(+), 201 deletions(-) create mode 100644 lua/blink-ripgrep/backends/ripgrep/ripgrep.lua rename lua/blink-ripgrep/{ => backends/ripgrep}/ripgrep_command.lua (100%) rename lua/blink-ripgrep/{ => backends/ripgrep}/ripgrep_parser.lua (100%) create mode 100644 lua/blink-ripgrep/documentation.lua diff --git a/lua/blink-ripgrep/backends/ripgrep/ripgrep.lua b/lua/blink-ripgrep/backends/ripgrep/ripgrep.lua new file mode 100644 index 0000000..d34a561 --- /dev/null +++ b/lua/blink-ripgrep/backends/ripgrep/ripgrep.lua @@ -0,0 +1,146 @@ +---@class blink-ripgrep.Backend +---@field config blink-ripgrep.Options +local RipgrepBackend = {} + +---@param config table +function RipgrepBackend.new(config) + local self = setmetatable({}, { __index = RipgrepBackend }) + self.config = config + return self --[[@as blink-ripgrep.Backend]] +end + +function RipgrepBackend:get_matches(prefix, context, resolve) + if self.config.mode ~= "on" then + if self.config.debug then + local debug = require("blink-ripgrep.debug") + debug.add_debug_message("mode is off, skipping the search") + debug.add_debug_invocation({ "ignored-because-mode-is-off" }) + end + resolve() + return + end + + if string.len(prefix) < self.config.prefix_min_len then + resolve() + return + end + + -- builtin default command + local command_module = + require("blink-ripgrep.backends.ripgrep.ripgrep_command") + local cmd = command_module.get_command(prefix, self.config) + + if cmd == nil then + if self.config.debug then + local debug = require("blink-ripgrep.debug") + debug.add_debug_message("no command returned, skipping the search") + debug.add_debug_invocation({ "ignored-because-no-command" }) + end + + resolve() + return + end + + if vim.tbl_contains(self.config.ignore_paths, cmd.root) then + if self.config.debug then + local debug = require("blink-ripgrep.debug") + debug.add_debug_message("skipping search in ignored path" .. cmd.root) + debug.add_debug_invocation({ "ignored", cmd.root }) + end + resolve() + + return + end + + if self.config.debug then + if cmd.debugify_for_shell then + cmd:debugify_for_shell() + end + + require("blink-ripgrep.visualization").flash_search_prefix(prefix) + require("blink-ripgrep.debug").add_debug_invocation(cmd) + end + + local rg = vim.system(cmd.command, nil, function(result) + vim.schedule(function() + if result.code ~= 0 then + resolve() + return + end + + local lines = vim.split(result.stdout, "\n") + local cwd = vim.uv.cwd() or "" + + local parsed = + require("blink-ripgrep.backends.ripgrep.ripgrep_parser").parse( + lines, + cwd, + self.config.context_size + ) + local kinds = require("blink.cmp.types").CompletionItemKind + + ---@type table + local items = {} + for _, file in pairs(parsed.files) do + for _, match in pairs(file.matches) do + local matchkey = match.match.text + + -- PERF: only register the match once - right now there is no useful + -- 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( + self.config, + draw_opts, + file, + match + ) + end + + ---@diagnostic disable-next-line: missing-fields + 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 + render = draw_docs, + }, + source_id = "blink-ripgrep", + kind = kinds.Text, + label = label, + insertText = matchkey, + } + end + end + end + + vim.schedule(function() + resolve({ + is_incomplete_forward = false, + is_incomplete_backward = false, + items = vim.tbl_values(items), + context = context, + }) + end) + end) + end) + + return function() + rg:kill(9) + if self.config.debug then + require("blink-ripgrep.debug").add_debug_message( + "killed previous invocation" + ) + end + end +end + +return RipgrepBackend diff --git a/lua/blink-ripgrep/ripgrep_command.lua b/lua/blink-ripgrep/backends/ripgrep/ripgrep_command.lua similarity index 100% rename from lua/blink-ripgrep/ripgrep_command.lua rename to lua/blink-ripgrep/backends/ripgrep/ripgrep_command.lua diff --git a/lua/blink-ripgrep/ripgrep_parser.lua b/lua/blink-ripgrep/backends/ripgrep/ripgrep_parser.lua similarity index 100% rename from lua/blink-ripgrep/ripgrep_parser.lua rename to lua/blink-ripgrep/backends/ripgrep/ripgrep_parser.lua diff --git a/lua/blink-ripgrep/documentation.lua b/lua/blink-ripgrep/documentation.lua new file mode 100644 index 0000000..0b36bd8 --- /dev/null +++ b/lua/blink-ripgrep/documentation.lua @@ -0,0 +1,70 @@ +local documentation = {} + +local highlight_ns_id = 0 +pcall(function() + highlight_ns_id = require("blink.cmp.config").appearance.highlight_ns +end) +vim.api.nvim_set_hl(0, "BlinkRipgrepMatch", { link = "Search", default = true }) + +---@param config blink-ripgrep.Options +---@param draw_opts blink.cmp.CompletionDocumentationDrawOpts +---@param file blink-ripgrep.RipgrepFile +---@param match blink-ripgrep.RipgrepMatch +function documentation.render_item_documentation(config, draw_opts, file, match) + local bufnr = draw_opts.window:get_buf() + ---@type string[] + local text = { + file.relative_to_cwd, + string.rep( + "─", + -- TODO account for the width of the scrollbar if it's visible + draw_opts.window:get_width() + - draw_opts.window:get_border_size().horizontal + - 1 + ), + } + for _, data in ipairs(match.context_preview) do + table.insert(text, data.text) + end + + -- TODO add extmark highlighting for the divider line like in blink + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, text) + + local filetype = vim.filetype.match({ filename = file.relative_to_cwd }) + local parser_name = vim.treesitter.language.get_lang(filetype or "") + local parser_installed = parser_name + and pcall(function() + return vim.treesitter.get_parser(nil, file.language, {}) + end) + + if not parser_installed and config.fallback_to_regex_highlighting then + -- Can't show highlighted text because no treesitter parser + -- has been installed for this language. + -- + -- Fall back to regex based highlighting that is bundled in + -- neovim. It might not be perfect but it's much better + -- than no colors at all + vim.schedule(function() + vim.api.nvim_set_option_value("filetype", file.language, { buf = bufnr }) + vim.api.nvim_buf_call(bufnr, function() + vim.cmd("syntax on") + end) + end) + else + assert(parser_name, "missing parser") -- lua-language-server should narrow this but can't + require("blink.cmp.lib.window.docs").highlight_with_treesitter( + bufnr, + parser_name, + 2, + #text + ) + end + + require("blink-ripgrep.highlighting").highlight_match_in_doc_window( + bufnr, + match, + highlight_ns_id + ) +end + +return documentation diff --git a/lua/blink-ripgrep/init.lua b/lua/blink-ripgrep/init.lua index 85cbf5f..53caeb1 100644 --- a/lua/blink-ripgrep/init.lua +++ b/lua/blink-ripgrep/init.lua @@ -27,6 +27,9 @@ ---| "on" # Show completions when triggered by blink ---| "off" # Don't show completions at all +---@class blink-ripgrep.Backend # a backend defines how to get matches from the project's files for a search +---@field get_matches fun(self: blink-ripgrep.Backend, prefix: string, context: blink.cmp.Context, resolve: fun(response: blink.cmp.CompletionResponse | nil)): nil | fun(): nil # start a search process. Return an optional cancellation function that kills the search in case the user has canceled the completion. + ---@class blink-ripgrep.RgSource : blink.cmp.Source ---@field get_command fun(context: blink.cmp.Context, prefix: string): blink-ripgrep.RipgrepCommand | nil ---@field get_prefix fun(context: blink.cmp.Context): string @@ -34,12 +37,6 @@ local RgSource = {} RgSource.__index = RgSource -local highlight_ns_id = 0 -pcall(function() - highlight_ns_id = require("blink.cmp.config").appearance.highlight_ns -end) -vim.api.nvim_set_hl(0, "BlinkRipgrepMatch", { link = "Search", default = true }) - ---@type blink-ripgrep.Options RgSource.config = { prefix_min_len = 3, @@ -88,202 +85,13 @@ function RgSource.new(input_opts) return self end ----@param opts blink.cmp.CompletionDocumentationDrawOpts ----@param file blink-ripgrep.RipgrepFile ----@param match blink-ripgrep.RipgrepMatch -local function render_item_documentation(opts, file, match) - local bufnr = opts.window:get_buf() - ---@type string[] - local text = { - file.relative_to_cwd, - string.rep( - "─", - -- TODO account for the width of the scrollbar if it's visible - opts.window:get_width() - - opts.window:get_border_size().horizontal - - 1 - ), - } - for _, data in ipairs(match.context_preview) do - table.insert(text, data.text) - end - - -- TODO add extmark highlighting for the divider line like in blink - vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, text) - - local filetype = vim.filetype.match({ filename = file.relative_to_cwd }) - local parser_name = vim.treesitter.language.get_lang(filetype or "") - local parser_installed = parser_name - and pcall(function() - return vim.treesitter.get_parser(nil, file.language, {}) - end) - - if - not parser_installed and RgSource.config.fallback_to_regex_highlighting - then - -- Can't show highlighted text because no treesitter parser - -- has been installed for this language. - -- - -- Fall back to regex based highlighting that is bundled in - -- neovim. It might not be perfect but it's much better - -- than no colors at all - vim.schedule(function() - vim.api.nvim_set_option_value("filetype", file.language, { buf = bufnr }) - vim.api.nvim_buf_call(bufnr, function() - vim.cmd("syntax on") - end) - end) - else - assert(parser_name, "missing parser") -- lua-language-server should narrow this but can't - require("blink.cmp.lib.window.docs").highlight_with_treesitter( - bufnr, - parser_name, - 2, - #text - ) - end - - require("blink-ripgrep.highlighting").highlight_match_in_doc_window( - bufnr, - match, - highlight_ns_id - ) -end - function RgSource:get_completions(context, resolve) - if RgSource.config.mode ~= "on" then - if RgSource.config.debug then - local debug = require("blink-ripgrep.debug") - debug.add_debug_message("mode is off, skipping the search") - debug.add_debug_invocation({ "ignored-because-mode-is-off" }) - end - resolve() - return - end - + local ripgrep = + require("blink-ripgrep.backends.ripgrep.ripgrep").new(RgSource.config) local prefix = self.get_prefix(context) + local cancellation_function = ripgrep:get_matches(prefix, context, resolve) - if string.len(prefix) < RgSource.config.prefix_min_len then - resolve() - return - end - - ---@type blink-ripgrep.RipgrepCommand | nil - local cmd - if self.get_command then - -- custom command provided by the user - cmd = self.get_command(context, prefix) - else - -- builtin default command - local command_module = require("blink-ripgrep.ripgrep_command") - cmd = command_module.get_command(prefix, RgSource.config) - end - - if cmd == nil then - if RgSource.config.debug then - local debug = require("blink-ripgrep.debug") - debug.add_debug_message("no command returned, skipping the search") - debug.add_debug_invocation({ "ignored-because-no-command" }) - end - - resolve() - return - end - - if vim.tbl_contains(RgSource.config.ignore_paths, cmd.root) then - if RgSource.config.debug then - local debug = require("blink-ripgrep.debug") - debug.add_debug_message("skipping search in ignored path" .. cmd.root) - debug.add_debug_invocation({ "ignored", cmd.root }) - end - resolve() - - return - end - - if RgSource.config.debug then - if cmd.debugify_for_shell then - cmd:debugify_for_shell() - end - - require("blink-ripgrep.visualization").flash_search_prefix(prefix) - require("blink-ripgrep.debug").add_debug_invocation(cmd) - end - - local rg = vim.system(cmd.command, nil, function(result) - vim.schedule(function() - if result.code ~= 0 then - resolve() - return - end - - local lines = vim.split(result.stdout, "\n") - local cwd = vim.uv.cwd() or "" - - local parsed = require("blink-ripgrep.ripgrep_parser").parse( - lines, - cwd, - RgSource.config.context_size - ) - local kinds = require("blink.cmp.types").CompletionItemKind - - ---@type table - local items = {} - for _, file in pairs(parsed.files) do - for _, match in pairs(file.matches) do - local matchkey = match.match.text - - -- PERF: only register the match once - right now there is no useful - -- 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(opts) - render_item_documentation(opts, file, match) - end - - ---@diagnostic disable-next-line: missing-fields - 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 - render = draw_docs, - }, - source_id = "blink-ripgrep", - kind = kinds.Text, - label = label, - insertText = matchkey, - } - end - end - end - - vim.schedule(function() - resolve({ - is_incomplete_forward = false, - is_incomplete_backward = false, - items = vim.tbl_values(items), - context = context, - }) - end) - end) - end) - - return function() - rg:kill(9) - if RgSource.config.debug then - require("blink-ripgrep.debug").add_debug_message( - "killed previous invocation" - ) - end - end + return cancellation_function end return RgSource diff --git a/spec/blink-ripgrep/get_command_spec.lua b/spec/blink-ripgrep/get_command_spec.lua index 350ae67..71589b3 100644 --- a/spec/blink-ripgrep/get_command_spec.lua +++ b/spec/blink-ripgrep/get_command_spec.lua @@ -1,6 +1,6 @@ local assert = require("luassert") local blink_ripgrep = require("blink-ripgrep") -local RipgrepCommand = require("blink-ripgrep.ripgrep_command") +local RipgrepCommand = require("blink-ripgrep.backends.ripgrep.ripgrep_command") describe("get_command", function() local default_config = vim.tbl_deep_extend("error", {}, blink_ripgrep.config) diff --git a/spec/blink-ripgrep/ripgrep_parser_spec.lua b/spec/blink-ripgrep/ripgrep_parser_spec.lua index 976f469..e2b4090 100644 --- a/spec/blink-ripgrep/ripgrep_parser_spec.lua +++ b/spec/blink-ripgrep/ripgrep_parser_spec.lua @@ -1,4 +1,4 @@ -local ripgrep_parser = require("blink-ripgrep.ripgrep_parser") +local ripgrep_parser = require("blink-ripgrep.backends.ripgrep.ripgrep_parser") local assert = require("luassert") describe("ripgrep_parser", function()