From a4fe487a14e0c83b0f8683294c05567b3dd6033a Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Fri, 20 Oct 2023 15:42:23 +0800 Subject: [PATCH 01/11] refactor(gitlinker): optimize code logic --- lua/gitlinker.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lua/gitlinker.lua b/lua/gitlinker.lua index 011a9e2..d3522a9 100644 --- a/lua/gitlinker.lua +++ b/lua/gitlinker.lua @@ -103,10 +103,8 @@ local function setup(option) }) local key_mappings = nil - if type(option) == "table" and option["mapping"] ~= nil then - if type(option["mapping"]) == "table" then - key_mappings = option["mapping"] - end + if type(option) == "table" and type(option["mapping"]) == "table" then + key_mappings = option["mapping"] else key_mappings = Defaults.mapping end From 1d1f7f9d27d2ded3a6f20099f2ed48a9b5f912d9 Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Fri, 20 Oct 2023 15:42:50 +0800 Subject: [PATCH 02/11] revert --- lua/gitlinker.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lua/gitlinker.lua b/lua/gitlinker.lua index d3522a9..011a9e2 100644 --- a/lua/gitlinker.lua +++ b/lua/gitlinker.lua @@ -103,8 +103,10 @@ local function setup(option) }) local key_mappings = nil - if type(option) == "table" and type(option["mapping"]) == "table" then - key_mappings = option["mapping"] + if type(option) == "table" and option["mapping"] ~= nil then + if type(option["mapping"]) == "table" then + key_mappings = option["mapping"] + end else key_mappings = Defaults.mapping end From b65e52d1c5ec108999470d05b18966660034874f Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Fri, 20 Oct 2023 15:43:31 +0800 Subject: [PATCH 03/11] fix: only set key mappings when options.mappings is lua table --- lua/gitlinker.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/gitlinker.lua b/lua/gitlinker.lua index 011a9e2..643e8c0 100644 --- a/lua/gitlinker.lua +++ b/lua/gitlinker.lua @@ -112,7 +112,7 @@ local function setup(option) end -- key mapping - if key_mappings then + if type(key_mappings) == "table" then for k, v in pairs(key_mappings) do local opt = { noremap = true, From 8523fa99f902cb1b8d3aff480420f2a20a1e3319 Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Fri, 20 Oct 2023 15:51:21 +0800 Subject: [PATCH 04/11] perf(git): add 'uv.spawn' --- lua/gitlinker/spawn.lua | 223 ++++++++++++++++++++++++++++++++++++++++ test/spawn_spec.lua | 49 +++++++++ 2 files changed, 272 insertions(+) create mode 100644 lua/gitlinker/spawn.lua create mode 100644 test/spawn_spec.lua diff --git a/lua/gitlinker/spawn.lua b/lua/gitlinker/spawn.lua new file mode 100644 index 0000000..ec3061c --- /dev/null +++ b/lua/gitlinker/spawn.lua @@ -0,0 +1,223 @@ +-- port from: https://github.com/linrongbin16/fzfx.nvim/main/lua/fzfx/utils.lua + +--- @param s string +--- @param t string +--- @param start integer? +--- @return integer? +local function _string_find(s, t, start) + -- start = start or 1 + -- local result = vim.fn.stridx(s, t, start - 1) + -- return result >= 0 and (result + 1) or nil + + start = start or 1 + for i = start, #s do + local match = true + for j = 1, #t do + if i + j - 1 > #s then + match = false + break + end + local a = string.byte(s, i + j - 1) + local b = string.byte(t, j) + if a ~= b then + match = false + break + end + end + if match then + return i + end + end + return nil +end + +--- @alias SpawnLineConsumer fun(line:string):any +--- @class Spawn +--- @field cmds string[] +--- @field fn_out_line_consumer SpawnLineConsumer +--- @field fn_err_line_consumer SpawnLineConsumer +--- @field out_pipe uv_pipe_t +--- @field err_pipe uv_pipe_t +--- @field out_buffer string? +--- @field err_buffer string? +--- @field process_handle uv_process_t? +--- @field process_id integer|string|nil +--- @field _close_count integer +--- @field result {code:integer?,signal:integer?}? +local Spawn = {} + +--- @param cmds string[] +--- @param fn_out_line_consumer SpawnLineConsumer +--- @param fn_err_line_consumer SpawnLineConsumer +--- @return Spawn? +function Spawn:make(cmds, fn_out_line_consumer, fn_err_line_consumer) + local out_pipe = vim.loop.new_pipe(false) --[[@as uv_pipe_t]] + local err_pipe = vim.loop.new_pipe(false) --[[@as uv_pipe_t]] + if not out_pipe or not err_pipe then + return nil + end + + local o = { + cmds = cmds, + fn_out_line_consumer = fn_out_line_consumer, + fn_err_line_consumer = fn_err_line_consumer, + out_pipe = out_pipe, + err_pipe = err_pipe, + out_buffer = nil, + err_buffer = nil, + process_handle = nil, + process_id = nil, + _close_count = 0, + result = nil, + } + setmetatable(o, self) + self.__index = self + return o +end + +--- @param buffer string +--- @param fn_line_processor SpawnLineConsumer +--- @return integer +function Spawn:_consume_line(buffer, fn_line_processor) + local i = 1 + while i <= #buffer do + local newline_pos = _string_find(buffer, "\n", i) + if not newline_pos then + break + end + local line = buffer:sub(i, newline_pos - 1) + fn_line_processor(line) + i = newline_pos + 1 + end + return i +end + +--- @param handle uv_handle_t +function Spawn:_close_handle(handle) + if handle and not handle:is_closing() then + handle:close(function() + self._close_count = self._close_count + 1 + if self._close_count >= 3 then + vim.loop.stop() + end + end) + end +end + +--- @param err string? +--- @param data string? +--- @return nil +function Spawn:_on_stdout(err, data) + if err then + self.out_pipe:read_stop() + self:_close_handle(self.out_pipe) + return + end + + if data then + -- append data to data_buffer + self.out_buffer = self.out_buffer and (self.out_buffer .. data) or data + self.out_buffer = self.out_buffer:gsub("\r\n", "\n") + -- foreach the data_buffer and find every line + local i = self:_consume_line(self.out_buffer, self.fn_out_line_consumer) + -- truncate the printed lines if found any + self.out_buffer = i <= #self.out_buffer + and self.out_buffer:sub(i, #self.out_buffer) + or nil + else + if self.out_buffer then + -- foreach the data_buffer and find every line + local i = + self:_consume_line(self.out_buffer, self.fn_out_line_consumer) + if i <= #self.out_buffer then + local line = self.out_buffer:sub(i, #self.out_buffer) + self.fn_out_line_consumer(line) + self.out_buffer = nil + end + end + self.out_pipe:read_stop() + self:_close_handle(self.out_pipe) + end +end + +--- @param err string? +--- @param data string? +--- @return nil +function Spawn:_on_stderr(err, data) + if err then + io.write( + string.format( + "AsyncSpawn:_on_stderr, err:%s, data:%s", + vim.inspect(err), + vim.inspect(data) + ) + ) + error( + string.format( + "AsyncSpawn:_on_stderr, err:%s, data:%s", + vim.inspect(err), + vim.inspect(data) + ) + ) + self.err_pipe:read_stop() + self:_close_handle(self.err_pipe) + return + end + + if data then + -- append data to data_buffer + self.err_buffer = self.err_buffer and (self.err_buffer .. data) or data + self.err_buffer = self.err_buffer:gsub("\r\n", "\n") + -- foreach the data_buffer and find every line + local i = self:_consume_line(self.err_buffer, self.fn_err_line_consumer) + -- truncate the printed lines if found any + self.err_buffer = i <= #self.err_buffer + and self.err_buffer:sub(i, #self.err_buffer) + or nil + else + if self.err_buffer then + -- foreach the data_buffer and find every line + local i = + self:_consume_line(self.err_buffer, self.fn_err_line_consumer) + if i <= #self.err_buffer then + local line = self.err_buffer:sub(i, #self.err_buffer) + self.fn_err_line_consumer(line) + self.err_buffer = nil + end + end + self.err_pipe:read_stop() + self:_close_handle(self.err_pipe) + end +end + +function Spawn:run() + self.process_handle, self.process_id = vim.loop.spawn(self.cmds[1], { + args = vim.list_slice(self.cmds, 2), + stdio = { nil, self.out_pipe, self.err_pipe }, + hide = true, + -- verbatim = true, + }, function(code, signal) + self.result = { code = code, signal = signal } + self:_close_handle(self.process_handle) + end) + + self.out_pipe:read_start(function(err, data) + self:_on_stdout(err, data) + end) + self.err_pipe:read_start(function(err, data) + self:_on_stderr(err, data) + end) + vim.loop.run() + + local max_timeout = 2 ^ 31 + vim.wait(max_timeout, function() + return self._close_count == 3 + end) +end + +local M = { + _string_find = _string_find, + Spawn = Spawn, +} + +return M diff --git a/test/spawn_spec.lua b/test/spawn_spec.lua new file mode 100644 index 0000000..092a1ea --- /dev/null +++ b/test/spawn_spec.lua @@ -0,0 +1,49 @@ +local cwd = vim.fn.getcwd() + +describe("spawn", function() + local assert_eq = assert.is_equal + local assert_true = assert.is_true + local assert_false = assert.is_false + + before_each(function() + vim.api.nvim_command("cd " .. cwd) + end) + + local spawn = require("gitlinker.spawn") + + describe("[_string_find]", function() + it("found", function() + assert_eq(spawn._string_find("abcdefg", "a"), 1) + assert_eq(spawn._string_find("abcdefg", "a", 1), 1) + assert_eq(spawn._string_find("abcdefg", "g"), 7) + assert_eq(spawn._string_find("abcdefg", "g", 1), 7) + assert_eq(spawn._string_find("abcdefg", "g", 7), 7) + assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--"), 6) + assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 1), 6) + assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 2), 6) + assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 3), 6) + assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 6), 6) + assert_eq(spawn._string_find("fzfx -w -- -g *.lua", "--"), 9) + assert_eq(spawn._string_find("fzfx -w -- -g *.lua", "--", 1), 9) + assert_eq(spawn._string_find("fzfx -w -- -g *.lua", "--", 2), 9) + assert_eq(spawn._string_find("fzfx -w ---g *.lua", "--", 8), 9) + assert_eq(spawn._string_find("fzfx -w ---g *.lua", "--", 9), 9) + end) + it("not found", function() + assert_eq(spawn._string_find("abcdefg", "a", 2), nil) + assert_eq(spawn._string_find("abcdefg", "a", 7), nil) + assert_eq(spawn._string_find("abcdefg", "g", 8), nil) + assert_eq(spawn._string_find("abcdefg", "g", 9), nil) + assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 7), nil) + assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 8), nil) + assert_eq(spawn._string_find("fzfx -w -- -g *.lua", "--", 10), nil) + assert_eq(spawn._string_find("fzfx -w -- -g *.lua", "--", 11), nil) + assert_eq(spawn._string_find("fzfx -w ---g *.lua", "--", 11), nil) + assert_eq(spawn._string_find("fzfx -w ---g *.lua", "--", 12), nil) + assert_eq(spawn._string_find("", "--"), nil) + assert_eq(spawn._string_find("", "--", 1), nil) + assert_eq(spawn._string_find("-", "--"), nil) + assert_eq(spawn._string_find("--", "---", 1), nil) + end) + end) +end) From bc2be26bf12d7818d285826606db18267974afa7 Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Fri, 20 Oct 2023 17:04:57 +0800 Subject: [PATCH 05/11] perf(git): add 'uv.spawn' --- lua/gitlinker/spawn.lua | 47 +++++++- test/spawn_spec.lua | 241 +++++++++++++++++++++++++++++++++++----- 2 files changed, 253 insertions(+), 35 deletions(-) diff --git a/lua/gitlinker/spawn.lua b/lua/gitlinker/spawn.lua index ec3061c..0d8a4b8 100644 --- a/lua/gitlinker/spawn.lua +++ b/lua/gitlinker/spawn.lua @@ -4,7 +4,7 @@ --- @param t string --- @param start integer? --- @return integer? -local function _string_find(s, t, start) +local function string_find(s, t, start) -- start = start or 1 -- local result = vim.fn.stridx(s, t, start - 1) -- return result >= 0 and (result + 1) or nil @@ -31,6 +31,32 @@ local function _string_find(s, t, start) return nil end +--- @param filename string +--- @param opts {trim:boolean?}|nil +--- @return string? +local function readfile(filename, opts) + opts = opts or { trim = true } + opts.trim = opts.trim == nil and true or opts.trim + + local f = io.open(filename, "r") + if f == nil then + return nil + end + local content = vim.trim(f:read("*a")) + f:close() + return content +end + +--- @param filename string +--- @return string[]? +local function readlines(filename) + local results = {} + for line in io.lines(filename) do + table.insert(results, line) + end + return results +end + --- @alias SpawnLineConsumer fun(line:string):any --- @class Spawn --- @field cmds string[] @@ -46,9 +72,17 @@ end --- @field result {code:integer?,signal:integer?}? local Spawn = {} +--- @param line string +local function dummy_stderr_line_consumer(line) + -- if type(line) == "string" then + -- io.write(string.format("AsyncSpawn:_on_stderr:%s", vim.inspect(line))) + -- error(string.format("AsyncSpawn:_on_stderr:%s", vim.inspect(line))) + -- end +end + --- @param cmds string[] --- @param fn_out_line_consumer SpawnLineConsumer ---- @param fn_err_line_consumer SpawnLineConsumer +--- @param fn_err_line_consumer SpawnLineConsumer? --- @return Spawn? function Spawn:make(cmds, fn_out_line_consumer, fn_err_line_consumer) local out_pipe = vim.loop.new_pipe(false) --[[@as uv_pipe_t]] @@ -60,7 +94,8 @@ function Spawn:make(cmds, fn_out_line_consumer, fn_err_line_consumer) local o = { cmds = cmds, fn_out_line_consumer = fn_out_line_consumer, - fn_err_line_consumer = fn_err_line_consumer, + fn_err_line_consumer = fn_err_line_consumer + or dummy_stderr_line_consumer, out_pipe = out_pipe, err_pipe = err_pipe, out_buffer = nil, @@ -81,7 +116,7 @@ end function Spawn:_consume_line(buffer, fn_line_processor) local i = 1 while i <= #buffer do - local newline_pos = _string_find(buffer, "\n", i) + local newline_pos = string_find(buffer, "\n", i) if not newline_pos then break end @@ -216,7 +251,9 @@ function Spawn:run() end local M = { - _string_find = _string_find, + string_find = string_find, + readfile = readfile, + readlines = readlines, Spawn = Spawn, } diff --git a/test/spawn_spec.lua b/test/spawn_spec.lua index 092a1ea..3d843c1 100644 --- a/test/spawn_spec.lua +++ b/test/spawn_spec.lua @@ -11,39 +11,220 @@ describe("spawn", function() local spawn = require("gitlinker.spawn") - describe("[_string_find]", function() + describe("[string_find]", function() it("found", function() - assert_eq(spawn._string_find("abcdefg", "a"), 1) - assert_eq(spawn._string_find("abcdefg", "a", 1), 1) - assert_eq(spawn._string_find("abcdefg", "g"), 7) - assert_eq(spawn._string_find("abcdefg", "g", 1), 7) - assert_eq(spawn._string_find("abcdefg", "g", 7), 7) - assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--"), 6) - assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 1), 6) - assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 2), 6) - assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 3), 6) - assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 6), 6) - assert_eq(spawn._string_find("fzfx -w -- -g *.lua", "--"), 9) - assert_eq(spawn._string_find("fzfx -w -- -g *.lua", "--", 1), 9) - assert_eq(spawn._string_find("fzfx -w -- -g *.lua", "--", 2), 9) - assert_eq(spawn._string_find("fzfx -w ---g *.lua", "--", 8), 9) - assert_eq(spawn._string_find("fzfx -w ---g *.lua", "--", 9), 9) + assert_eq(spawn.string_find("abcdefg", "a"), 1) + assert_eq(spawn.string_find("abcdefg", "a", 1), 1) + assert_eq(spawn.string_find("abcdefg", "g"), 7) + assert_eq(spawn.string_find("abcdefg", "g", 1), 7) + assert_eq(spawn.string_find("abcdefg", "g", 7), 7) + assert_eq(spawn.string_find("fzfx -- -w -g *.lua", "--"), 6) + assert_eq(spawn.string_find("fzfx -- -w -g *.lua", "--", 1), 6) + assert_eq(spawn.string_find("fzfx -- -w -g *.lua", "--", 2), 6) + assert_eq(spawn.string_find("fzfx -- -w -g *.lua", "--", 3), 6) + assert_eq(spawn.string_find("fzfx -- -w -g *.lua", "--", 6), 6) + assert_eq(spawn.string_find("fzfx -w -- -g *.lua", "--"), 9) + assert_eq(spawn.string_find("fzfx -w -- -g *.lua", "--", 1), 9) + assert_eq(spawn.string_find("fzfx -w -- -g *.lua", "--", 2), 9) + assert_eq(spawn.string_find("fzfx -w ---g *.lua", "--", 8), 9) + assert_eq(spawn.string_find("fzfx -w ---g *.lua", "--", 9), 9) end) it("not found", function() - assert_eq(spawn._string_find("abcdefg", "a", 2), nil) - assert_eq(spawn._string_find("abcdefg", "a", 7), nil) - assert_eq(spawn._string_find("abcdefg", "g", 8), nil) - assert_eq(spawn._string_find("abcdefg", "g", 9), nil) - assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 7), nil) - assert_eq(spawn._string_find("fzfx -- -w -g *.lua", "--", 8), nil) - assert_eq(spawn._string_find("fzfx -w -- -g *.lua", "--", 10), nil) - assert_eq(spawn._string_find("fzfx -w -- -g *.lua", "--", 11), nil) - assert_eq(spawn._string_find("fzfx -w ---g *.lua", "--", 11), nil) - assert_eq(spawn._string_find("fzfx -w ---g *.lua", "--", 12), nil) - assert_eq(spawn._string_find("", "--"), nil) - assert_eq(spawn._string_find("", "--", 1), nil) - assert_eq(spawn._string_find("-", "--"), nil) - assert_eq(spawn._string_find("--", "---", 1), nil) + assert_eq(spawn.string_find("abcdefg", "a", 2), nil) + assert_eq(spawn.string_find("abcdefg", "a", 7), nil) + assert_eq(spawn.string_find("abcdefg", "g", 8), nil) + assert_eq(spawn.string_find("abcdefg", "g", 9), nil) + assert_eq(spawn.string_find("fzfx -- -w -g *.lua", "--", 7), nil) + assert_eq(spawn.string_find("fzfx -- -w -g *.lua", "--", 8), nil) + assert_eq(spawn.string_find("fzfx -w -- -g *.lua", "--", 10), nil) + assert_eq(spawn.string_find("fzfx -w -- -g *.lua", "--", 11), nil) + assert_eq(spawn.string_find("fzfx -w ---g *.lua", "--", 11), nil) + assert_eq(spawn.string_find("fzfx -w ---g *.lua", "--", 12), nil) + assert_eq(spawn.string_find("", "--"), nil) + assert_eq(spawn.string_find("", "--", 1), nil) + assert_eq(spawn.string_find("-", "--"), nil) + assert_eq(spawn.string_find("--", "---", 1), nil) + end) + end) + describe("[Spawn]", function() + it("open", function() + local sp = spawn.Spawn:make({ "cat", "README.md" }, function() end) --[[@as Spawn]] + assert_eq(type(sp), "table") + assert_eq(type(sp.cmds), "table") + assert_eq(#sp.cmds, 2) + assert_eq(sp.cmds[1], "cat") + assert_eq(sp.cmds[2], "README.md") + assert_eq(type(sp.out_pipe), "userdata") + assert_eq(type(sp.err_pipe), "userdata") + end) + it("consume line", function() + local content = spawn.readfile("README.md") --[[@as string]] + local lines = spawn.readlines("README.md") --[[@as table]] + + local i = 1 + local function process_line(line) + print(string.format("actual:[%d]%s\n", i, line)) + print(string.format("expect:[%d]%s\n", i, lines[i])) + assert_eq(type(line), "string") + assert_eq(line, lines[i]) + i = i + 1 + end + local sp = spawn.Spawn:make({ "cat", "README.md" }, process_line) --[[@as Spawn]] + local pos = sp:_consume_line(content, process_line) + if pos <= #content then + local line = content:sub(pos, #content) + process_line(line) + end + end) + it("stdout on newline", function() + local content = spawn.readfile("README.md") --[[@as string]] + local lines = spawn.readlines("README.md") --[[@as table]] + + local i = 1 + local function process_line(line) + -- print(string.format("[%d]%s\n", i, line)) + assert_eq(type(line), "string") + assert_eq(line, lines[i]) + i = i + 1 + end + local sp = spawn.Spawn:make({ "cat", "README.md" }, process_line) --[[@as Spawn]] + local content_splits = + vim.split(content, "\n", { plain = true, trimempty = false }) + for j, splits in ipairs(content_splits) do + sp:_on_stdout(nil, splits) + if j < #content_splits then + sp:_on_stdout(nil, "\n") + end + end + sp:_on_stdout(nil, nil) + assert_true(sp.out_pipe:is_closing()) + end) + it("stdout on whitespace", function() + local content = spawn.readfile("README.md") --[[@as string]] + local lines = spawn.readlines("README.md") --[[@as table]] + + local i = 1 + local function process_line(line) + -- print(string.format("[%d]%s\n", i, line)) + assert_eq(type(line), "string") + assert_eq(line, lines[i]) + i = i + 1 + end + local sp = spawn.Spawn:make({ "cat", "README.md" }, process_line) --[[@as Spawn]] + local content_splits = + vim.split(content, " ", { plain = true, trimempty = false }) + for j, splits in ipairs(content_splits) do + sp:_on_stdout(nil, splits) + if j < #content_splits then + sp:_on_stdout(nil, " ") + end + end + sp:_on_stdout(nil, nil) + assert_true(sp.out_pipe:is_closing()) + end) + for delimiter_i = 0, 25 do + -- lower case: a + local lower_char = string.char(97 + delimiter_i) + it(string.format("stdout on %s", lower_char), function() + local content = spawn.readfile("README.md") --[[@as string]] + local lines = spawn.readlines("README.md") --[[@as table]] + + local i = 1 + local function process_line(line) + -- print(string.format("[%d]%s\n", i, line)) + assert_eq(type(line), "string") + assert_eq(line, lines[i]) + i = i + 1 + end + local sp = + spawn.Spawn:make({ "cat", "README.md" }, process_line) --[[@as Spawn]] + local content_splits = vim.split( + content, + lower_char, + { plain = true, trimempty = false } + ) + for j, splits in ipairs(content_splits) do + sp:_on_stdout(nil, splits) + if j < #content_splits then + sp:_on_stdout(nil, lower_char) + end + end + sp:_on_stdout(nil, nil) + assert_true(sp.out_pipe:is_closing()) + end) + -- upper case: A + local upper_char = string.char(65 + delimiter_i) + it(string.format("stdout on %s", upper_char), function() + local content = spawn.readfile("README.md") --[[@as string]] + local lines = spawn.readlines("README.md") --[[@as table]] + + local i = 1 + local function process_line(line) + -- print(string.format("[%d]%s\n", i, line)) + assert_eq(type(line), "string") + assert_eq(line, lines[i]) + i = i + 1 + end + local sp = + spawn.Spawn:make({ "cat", "README.md" }, process_line) --[[@as Spawn]] + local content_splits = vim.split( + content, + upper_char, + { plain = true, trimempty = false } + ) + for j, splits in ipairs(content_splits) do + sp:_on_stdout(nil, splits) + if j < #content_splits then + sp:_on_stdout(nil, upper_char) + end + end + sp:_on_stdout(nil, nil) + assert_true(sp.out_pipe:is_closing()) + end) + end + it("stderr", function() + local sp = spawn.Spawn:make({ "cat", "README.md" }, function() end) --[[@as Spawn]] + sp:_on_stderr(nil, nil) + assert_true(sp.err_pipe:is_closing()) + end) + it("iterate on README.md", function() + local lines = spawn.readlines("README.md") --[[@as table]] + + local i = 1 + local function process_line(line) + print(string.format("[%d]%s\n", i, line)) + assert_eq(type(line), "string") + assert_eq(lines[i], line) + i = i + 1 + end + + local sp = spawn.Spawn:make({ "cat", "README.md" }, process_line) --[[@as Spawn]] + sp:run() + end) + it("iterate on lua/gitlinker.lua", function() + local lines = spawn.readlines("lua/gitlinker.lua") --[[@as table]] + + local i = 1 + local function process_line(line) + print(string.format("[%d]%s\n", i, line)) + assert_eq(type(line), "string") + assert_eq(lines[i], line) + i = i + 1 + end + + local sp = + spawn.Spawn:make({ "cat", "lua/gitlinker.lua" }, process_line) --[[@as Spawn]] + sp:run() + end) + it("close handle", function() + local sp = spawn.Spawn:make( + { "cat", "lua/gitlinker.lua" }, + function() end + ) --[[@as Spawn]] + sp:run() + assert_true(sp.process_handle ~= nil) + sp:_close_handle(sp.process_handle) + assert_true(sp.process_handle:is_closing()) end) end) end) From 633c39ae7f2f2e85a46aae66b2e88b69f6267941 Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Fri, 20 Oct 2023 17:11:37 +0800 Subject: [PATCH 06/11] perf(git): migrate from jobstart to uv.spawn --- lua/gitlinker/git.lua | 65 +++++++++++-------------------------------- 1 file changed, 17 insertions(+), 48 deletions(-) diff --git a/lua/gitlinker/git.lua b/lua/gitlinker/git.lua index ca23e4a..941c53c 100644 --- a/lua/gitlinker/git.lua +++ b/lua/gitlinker/git.lua @@ -1,8 +1,5 @@ local logger = require("gitlinker.logger") - ---- @class JobResult ---- @field stdout string[] ---- @field stderr string[] +local spawn = require("gitlinker.spawn") --- @param result JobResult --- @return boolean @@ -37,51 +34,23 @@ end --- @param cwd string|nil --- @return JobResult local function cmd(args, cwd) + --- @class JobResult + --- @field stdout string[] + --- @field stderr string[] + --- @type JobResult local result = { stdout = {}, stderr = {} } - local job = vim.fn.jobstart(args, { - cwd = cwd, - on_stdout = function(chanid, data, name) - logger.debug( - "|cmd.on_stdout| args(%s):%s, cwd(%s):%s, chanid(%s):%s, data(%s):%s, name(%s):%s", - vim.inspect(type(args)), - vim.inspect(args), - vim.inspect(type(cwd)), - vim.inspect(cwd), - vim.inspect(type(chanid)), - vim.inspect(chanid), - vim.inspect(type(data)), - vim.inspect(data), - vim.inspect(type(name)), - vim.inspect(name) - ) - for _, line in ipairs(data) do - if string.len(line) > 0 then - table.insert(result.stdout, line) - end - end - end, - on_stderr = function(chanid, data, name) - logger.debug( - "|cmd.on_stderr| args(%s):%s, cwd(%s):%s, chanid(%s):%s, data(%s):%s, name(%s):%s", - vim.inspect(type(args)), - vim.inspect(args), - vim.inspect(type(cwd)), - vim.inspect(cwd), - vim.inspect(type(chanid)), - vim.inspect(chanid), - vim.inspect(type(data)), - vim.inspect(data), - vim.inspect(type(name)), - vim.inspect(name) - ) - for _, line in ipairs(data) do - if string.len(line) > 0 then - table.insert(result.stderr, line) - end - end - end, - }) - vim.fn.jobwait({ job }) + + local sp = spawn.Spawn:make(args, function(line) + if type(line) == "string" then + table.insert(result.stdout, line) + end + end, function(line) + if type(line) == "string" then + table.insert(result.stderr, line) + end + end) --[[@as Spawn]] + sp:run() + logger.debug( "|cmd| args(%s):%s, cwd(%s):%s, result(%s):%s", vim.inspect(type(args)), From 40300edd9861c8d0bbb233e5fe9cfdc253c4523a Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Fri, 20 Oct 2023 17:21:42 +0800 Subject: [PATCH 07/11] refactor(git): rewrite JobReslt --- lua/gitlinker.lua | 14 ++++----- lua/gitlinker/git.lua | 66 ++++++++++++++++++++++--------------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/lua/gitlinker.lua b/lua/gitlinker.lua index 643e8c0..72ea04b 100644 --- a/lua/gitlinker.lua +++ b/lua/gitlinker.lua @@ -162,8 +162,8 @@ end local function make_link_data(range) --- @type JobResult local root_result = git.get_root() - if not git.result_has_out(root_result) then - git.result_print_err(root_result, "not in a git repository") + if not root_result:has_out() then + root_result:print_err("not in a git repository") return nil end logger.debug( @@ -185,9 +185,8 @@ local function make_link_data(range) --- @type JobResult local remote_url_result = git.get_remote_url(remote) - if not git.result_has_out(remote_url_result) then - git.result_print_err( - remote_url_result, + if not remote_url_result:has_out() then + remote_url_result:print_err( "failed to get remote url by remote '" .. remote .. "'" ) return nil @@ -221,9 +220,8 @@ local function make_link_data(range) --- @type JobResult local file_in_rev_result = git.is_file_in_rev(buf_path_on_root, rev) - if git.result_has_err(file_in_rev_result) then - git.result_print_err( - file_in_rev_result, + if git.file_in_rev_result:has_err() then + file_in_rev_result:print_err( "'" .. buf_path_on_root .. "' does not exist in remote '" diff --git a/lua/gitlinker/git.lua b/lua/gitlinker/git.lua index 941c53c..ff8a022 100644 --- a/lua/gitlinker/git.lua +++ b/lua/gitlinker/git.lua @@ -1,28 +1,38 @@ local logger = require("gitlinker.logger") local spawn = require("gitlinker.spawn") ---- @param result JobResult +--- @class JobResult +--- @field stdout string[] +--- @field stderr string[] +local JobResult = {} + +--- @return JobResult +function JobResult:new() + local o = { + stdout = {}, + stderr = {}, + } + setmetatable(o, self) + self.__index = self + return o +end + --- @return boolean -local function result_has_out(result) - return result["stdout"] - and type(result["stdout"]) == "table" - and #result["stdout"] > 0 +function JobResult:has_out() + return type(self.stdout) == "table" and #self.stdout > 0 end ---- @param result JobResult --- @return boolean -local function result_has_err(result) - return result["stderr"] ~= nil - and type(result["stderr"]) == "table" - and #result["stderr"] > 0 +function JobResult:has_err() + return type(self.stderr) == "table" and #self.stderr > 0 end ---- @param result JobResult ---- @param default string|nil ---- @return nil -local function result_print_err(result, default) - if result_has_err(result) and #result.stderr > 0 then - logger.err("%s", result.stderr[1]) +--- @param default string +function JobResult:print_err(default) + if self:has_err() then + for _, e in ipairs(self.stderr) do + logger.err("%s", e) + end else logger.err("fatal: %s", default) end @@ -34,11 +44,7 @@ end --- @param cwd string|nil --- @return JobResult local function cmd(args, cwd) - --- @class JobResult - --- @field stdout string[] - --- @field stderr string[] - --- @type JobResult - local result = { stdout = {}, stderr = {} } + local result = JobResult:new() local sp = spawn.Spawn:make(args, function(line) if type(line) == "string" then @@ -92,7 +98,7 @@ end --- @package --- @param revspec string|nil ---- @return string|nil +--- @return string? local function get_rev(revspec) local result = cmd({ "git", "rev-parse", revspec }) logger.debug( @@ -102,7 +108,7 @@ local function get_rev(revspec) vim.inspect(type(result)), vim.inspect(result) ) - return result_has_out(result) and result.stdout[1] or nil + return result:has_out() and result.stdout[1] or nil end --- @package @@ -151,7 +157,7 @@ local function has_file_changed(file, rev) vim.inspect(type(result)), vim.inspect(result) ) - return result_has_out(result) + return result:has_out() end --- @package @@ -240,14 +246,14 @@ local function get_root() return result end ---- @return string|nil +--- @return string? local function get_branch_remote() -- origin/upstream --- @type JobResult local remote_result = get_remote() if type(remote_result.stdout) ~= "table" or #remote_result.stdout == 0 then - result_print_err(remote_result, "git repository has no remote") + remote_result:print_err("git repository has no remote") return nil end @@ -258,8 +264,8 @@ local function get_branch_remote() -- origin/linrongbin16/add-rule2 --- @type JobResult local upstream_branch_result = get_rev_name("@{u}") - if not result_has_out(upstream_branch_result) then - result_print_err(upstream_branch_result, "git branch has no remote") + if not upstream_branch_result:has_out() then + upstream_branch_result:print_err("git branch has no remote") return nil end @@ -293,11 +299,7 @@ local function get_branch_remote() return nil end ---- @type table local M = { - result_has_out = result_has_out, - result_has_err = result_has_err, - result_print_err = result_print_err, get_root = get_root, get_remote_url = get_remote_url, is_file_in_rev = is_file_in_rev, From e108639ac1df138652acf31ea0092242ccd07d8a Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Fri, 20 Oct 2023 17:22:13 +0800 Subject: [PATCH 08/11] fix --- lua/gitlinker.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/gitlinker.lua b/lua/gitlinker.lua index 72ea04b..e58b4a9 100644 --- a/lua/gitlinker.lua +++ b/lua/gitlinker.lua @@ -220,7 +220,7 @@ local function make_link_data(range) --- @type JobResult local file_in_rev_result = git.is_file_in_rev(buf_path_on_root, rev) - if git.file_in_rev_result:has_err() then + if file_in_rev_result:has_err() then file_in_rev_result:print_err( "'" .. buf_path_on_root From de38613adba003e9771019bbd06ca808b4fd4b3d Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Fri, 20 Oct 2023 17:24:38 +0800 Subject: [PATCH 09/11] chore --- lua/gitlinker.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/gitlinker.lua b/lua/gitlinker.lua index e58b4a9..b668932 100644 --- a/lua/gitlinker.lua +++ b/lua/gitlinker.lua @@ -6,6 +6,7 @@ local logger = require("gitlinker.logger") --- @type Configs local Defaults = { -- print message(git host url) in command line + -- --- @type boolean message = true, @@ -24,8 +25,8 @@ local Defaults = { }, }, - -- regex pattern based rules - --- @type table[] + -- pattern based rules + --- @type {[1]:table,[2]:table} pattern_rules = { { ["^git@github%.([_%.%-%w]+):([%.%-%w]+)/([_%.%-%w]+)%.git$"] = "https://github.%1/%2/%3/blob/", From a0a9bd9430d12f0ccae3b8231f345ea9e513ac51 Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Fri, 20 Oct 2023 17:25:38 +0800 Subject: [PATCH 10/11] test --- lua/gitlinker.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/gitlinker.lua b/lua/gitlinker.lua index b668932..3e6bcd6 100644 --- a/lua/gitlinker.lua +++ b/lua/gitlinker.lua @@ -2,8 +2,8 @@ local git = require("gitlinker.git") local util = require("gitlinker.util") local logger = require("gitlinker.logger") ---- @alias Configs table ---- @type Configs +--- @alias Options table +--- @type Options local Defaults = { -- print message(git host url) in command line -- @@ -89,10 +89,10 @@ local Defaults = { file_log = false, } ---- @type Configs +--- @type Options local Configs = {} ---- @param option Configs? +--- @param option Options? local function setup(option) Configs = vim.tbl_deep_extend("force", Defaults, option or {}) From 65f4b7441efdb1acecfdb4eb3002206d82e9395e Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Fri, 20 Oct 2023 17:30:02 +0800 Subject: [PATCH 11/11] chore --- .luacov | 4 ++++ codecov.yml | 8 ++++++++ 2 files changed, 12 insertions(+) create mode 100644 .luacov create mode 100644 codecov.yml diff --git a/.luacov b/.luacov new file mode 100644 index 0000000..ea7f402 --- /dev/null +++ b/.luacov @@ -0,0 +1,4 @@ +modules = { + ["gitlinker"] = "lua/gitlinker.lua", + ["gitlinker.*"] = "lua", +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..e938135 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + threshold: 90% + patch: + default: + threshold: 90%