diff --git a/doc/cmp-rg.txt b/doc/cmp-rg.txt index 75352a4..529aab5 100644 --- a/doc/cmp-rg.txt +++ b/doc/cmp-rg.txt @@ -26,20 +26,20 @@ additional_arguments *cmp-rg-additional_arguments* Any additional arguments you want to pass to ripgrep. - Default: "" ~ + Default: {} ~ Example: > - { name = 'rg', option = { additional_arguments = "--smart-case" } } + { name = 'rg', option = { additional_arguments = { "--smart-case" } } } < Search in hidden files (starting with a `.`): > - { name = 'rg', option = { additional_arguments = "--hidden" } } + { name = 'rg', option = { additional_arguments = { "--hidden" } } } < Reduce the level of recursion into directories: > - { name = 'rg', option = { additional_arguments = "--max-depth 4" } } + { name = 'rg', option = { additional_arguments = { "--max-depth", "4" } } } < ------------------------------------------------------------------------------ diff --git a/lua/cmp-rg/init.lua b/lua/cmp-rg/init.lua index 99d03c8..2461e03 100644 --- a/lua/cmp-rg/init.lua +++ b/lua/cmp-rg/init.lua @@ -1,13 +1,19 @@ require "cmp-rg.types" +local Process = require "cmp-rg.process" +local uv = vim.uv or vim.loop + +local function show_error(msg) + vim.notify("[cmp-rg] " .. msg, vim.log.levels.ERROR) +end + ---@class Source ----@field public running_job_id number ----@field public json_decode fun(s: string): rg.Message +---@field public process table ---@field public timer any local source = {} source.new = function() - local timer = vim.loop.new_timer() + local timer = uv.new_timer() vim.api.nvim_create_autocmd("VimLeavePre", { callback = function() if timer and not timer:is_closing() then @@ -17,27 +23,25 @@ source.new = function() end, }) return setmetatable({ - running_job_id = 0, timer = timer, - json_decode = vim.fn.has "nvim-0.6" == 1 and vim.json.decode or vim.fn.json_decode, }, { __index = source }) end source.complete = function(self, request, callback) local q = string.sub(request.context.cursor_before_line, request.offset) local pattern = request.option.pattern or "[\\w_-]+" - local additional_arguments = request.option.additional_arguments or "" + local additional_arguments = request.option.additional_arguments or {} + if type(additional_arguments) == "string" then + show_error('Now additional_arguments must be an array of string') + additional_arguments = {} + end local context_before = request.option.context_before or 1 local context_after = request.option.context_after or 3 - local quote = "'" - if vim.o.shell == "cmd.exe" then - quote = '"' - end local seen = {} local items = {} local chunk_size = 5 - local function on_event(_, data, event) + local function on_event(data, event) if event == "stdout" then ---@type (string|rg.Message)[] local messages = data @@ -56,7 +60,7 @@ source.complete = function(self, request, callback) return nil end if type(m) == "string" then - local ok, decoded = pcall(self.json_decode, m) + local ok, decoded = pcall(vim.json.decode, m) if not ok then return nil end @@ -123,7 +127,7 @@ source.complete = function(self, request, callback) end if request.max_item_count ~= nil and #items >= request.max_item_count then - vim.fn.jobstop(self.running_job_id) + self.process:kill() callback { items = items, isIncomplete = false } return end @@ -135,9 +139,7 @@ source.complete = function(self, request, callback) end if event == "stderr" and request.option.debug then - vim.cmd "echohl Error" - vim.cmd('echomsg "' .. table.concat(data, "") .. '"') - vim.cmd "echohl None" + show_error(table.concat(data, "")) end if event == "exit" then @@ -146,31 +148,30 @@ source.complete = function(self, request, callback) end self.timer:stop() - self.timer:start( - request.option.debounce or 100, - 0, - vim.schedule_wrap(function() - vim.fn.jobstop(self.running_job_id) - self.running_job_id = vim.fn.jobstart( - string.format( - "rg --heading --json --word-regexp -B %d -A %d --color never %s %s%s%s%s .", - context_before, - context_after, - additional_arguments, - quote, - q, - pattern, - quote - ), - { - on_stderr = on_event, - on_stdout = on_event, - on_exit = on_event, - cwd = request.option.cwd or vim.fn.getcwd(), - } - ) - end) - ) + self.timer:start(request.option.debounce or 100, 0, function() + if self.process then + self.process:kill() + end + local args = { + "--heading", + "--json", + "--word-regexp", + "-B", + tostring(context_before), + "-A", + tostring(context_after), + "--color", + "never", + } + if #additional_arguments > 0 then + for _, v in ipairs(additional_arguments) do + table.insert(args, v) + end + end + table.insert(args,q .. pattern) + table.insert(args,".") + self.process = Process.new("rg", args, on_event, { cwd = request.option.cwd }):run() + end) end return source diff --git a/lua/cmp-rg/process.lua b/lua/cmp-rg/process.lua new file mode 100644 index 0000000..5967fba --- /dev/null +++ b/lua/cmp-rg/process.lua @@ -0,0 +1,112 @@ +local uv = vim.uv or vim.loop +local debug = require "cmp.utils.debug" + +---@alias rg.Callback fun(data: string[], event: "stdout"|"stderr"|"exit"): nil + +---@class rg.Pipe +---@field pipe uv_pipe_t +---@field private event string +---@field private callback rg.Callback +---@field private is_closed boolean +---@field private tmp_out string +local Pipe = {} + +---@param event string +---@param callback rg.Callback +---@return rg.Pipe +Pipe.new = function(event, callback) + return setmetatable({ event = event, callback = callback, pipe = uv.new_pipe(), tmp_out = "" }, { __index = Pipe }) +end + +function Pipe:close() + if not self.is_closed then + self.pipe:close() + self.is_closed = true + end +end + +function Pipe:read_start() + self.pipe:read_start(function(err, chunk) + assert(not err, err) + if not chunk then + return + end + self.tmp_out = self.tmp_out .. chunk:gsub("\r\n", "\n") + local lines = vim.split(self.tmp_out, "\n", { plain = true }) + if #lines > 1 then + self.tmp_out = lines[#lines] + local data = {} + for i = 1, #lines - 1 do + data[i] = lines[i] + end + self.callback(data, self.event) + end + end) +end + +---@class rg.Process +---@field callback rg.Callback +---@field private handle uv_process_t? +---@field private cmd string +---@field private args string[] +---@field private stdout rg.Pipe +---@field private stderr rg.Pipe +---@field private cwd string +local Process = {} + +---@param cmd string +---@param args string[] +---@param callback rg.Callback +---@param options { cwd: string }? +---@return rg.Process +Process.new = function(cmd, args, callback, options) + options = vim.tbl_extend("force", { cwd = uv.cwd() }, options or {}) + return setmetatable({ + cmd = cmd, + args = args, + callback = callback, + stdout = Pipe.new("stdout", callback), + stderr = Pipe.new("stderr", callback), + cwd = options.cwd, + }, { __index = Process }) +end + +---@return rg.Process +function Process:run() + local err + self.handle, err = uv.spawn(self.cmd, { + args = self.args, + cwd = self.cwd, + stdio = { nil, self.stdout.pipe, self.stderr.pipe }, + }, function(_, _) + self:pipe_close() + if self.handle and not self.handle:is_closing() then + self.handle:close() + end + self.callback({}, "exit") + end) + if not self.handle then + self:pipe_close() + debug.log("rg", "process cannot spawn", { cmd = self.cmd, args = self.args, err = err }) + else + self.stdout:read_start() + self.stderr:read_start() + end + return self +end + +function Process:kill() + if self.handle then + self:pipe_close() + if not self.handle:is_closing() then + self.handle:close() + end + end +end + +function Process:pipe_close() + self.stdout:close() + self.stderr:close() +end + +return Process