diff --git a/.github/workflows/bump-commons.yml b/.github/workflows/bump-commons.yml
index e1d6bde1..79dc442a 100644
--- a/.github/workflows/bump-commons.yml
+++ b/.github/workflows/bump-commons.yml
@@ -19,10 +19,10 @@ jobs:
git clone --depth=1 https://github.com/linrongbin16/commons.nvim.git ~/.commons.nvim
rm -rf ./lua/gitlinker/commons
mkdir -p ./lua/gitlinker/commons
- cp -rf ~/.commons.nvim/lua/commons/*.lua ./lua/gitlinker/commons
- cp ~/.commons.nvim/version.txt ./lua/gitlinker/commons/version.txt
+ cp -rf ~/.commons.nvim/lua/commons ./lua/gitlinker
cd ./lua/gitlinker/commons
find . -type f -name '*.lua' -exec sed -i 's/require("commons/require("gitlinker.commons/g' {} \;
+ find . -type f -name '*.lua' -exec sed -i "s/require('commons/require('gitlinker.commons/g" {} \;
- uses: stefanzweifel/git-auto-commit-action@v5
if: ${{ github.ref != 'refs/heads/master' }}
with:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 97a85791..807a26c5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,8 +40,6 @@ jobs:
selene --config selene.toml ./lua
unit_test:
name: Unit Test
- needs:
- - lint
strategy:
matrix:
nvim_version: [stable, nightly]
diff --git a/.luarc.json b/.luarc.json
index 44964069..c040b727 100644
--- a/.luarc.json
+++ b/.luarc.json
@@ -7,7 +7,10 @@
"jit",
"utf8"
],
- "diagnostics.disable": [],
+ "diagnostics.disable": [
+ "luadoc-miss-symbol",
+ "missing-return"
+ ],
"runtime.version": "LuaJIT",
"workspace.checkThirdParty": "Disable"
}
diff --git a/README.md b/README.md
index 101feee5..ff19a3ac 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@
-
diff --git a/lua/gitlinker.lua b/lua/gitlinker.lua
index 81437d18..ed34d690 100644
--- a/lua/gitlinker.lua
+++ b/lua/gitlinker.lua
@@ -1,10 +1,9 @@
local tbl = require("gitlinker.commons.tbl")
local str = require("gitlinker.commons.str")
local num = require("gitlinker.commons.num")
-local LogLevels = require("gitlinker.commons.logging").LogLevels
-local logging = require("gitlinker.commons.logging")
+local log = require("gitlinker.commons.log")
+local async = require("gitlinker.commons.async")
-local async = require("gitlinker.async")
local configs = require("gitlinker.configs")
local range = require("gitlinker.range")
local linker = require("gitlinker.linker")
@@ -20,8 +19,6 @@ local function _url_template_engine(lk, template)
return nil
end
- local logger = logging.get("gitlinker")
-
--- @alias gitlinker.UrlTemplateExpr {plain:boolean,body:string}
--- @type gitlinker.UrlTemplateExpr[]
local exprs = {}
@@ -36,7 +33,7 @@ local function _url_template_engine(lk, template)
end
table.insert(exprs, { plain = true, body = string.sub(template, i, open_pos - 1) })
local close_pos = str.find(template, CLOSE_BRACE, open_pos + string.len(OPEN_BRACE))
- logger:ensure(
+ log.ensure(
type(close_pos) == "number" and close_pos > open_pos,
string.format(
"failed to evaluate url template(%s) at pos %d",
@@ -82,7 +79,7 @@ local function _url_template_engine(lk, template)
DEFAULT_BRANCH = str.not_empty(lk.default_branch) and lk.default_branch or "",
CURRENT_BRANCH = str.not_empty(lk.current_branch) and lk.current_branch or "",
})
- logger:debug(
+ log.debug(
string.format(
"|_url_template_engine| exp:%s, lk:%s, evaluated:%s",
vim.inspect(exp.body),
@@ -107,8 +104,7 @@ local function _worker(lk, p, r)
elseif type(r) == "string" then
return _url_template_engine(lk, r)
else
- local logger = logging.get("gitlinker")
- logger:ensure(
+ log.ensure(
false,
string.format("unsupported router %s on pattern %s", vim.inspect(r), vim.inspect(p))
)
@@ -121,17 +117,16 @@ end
--- @param lk gitlinker.Linker
--- @return string?
local function _router(router_type, lk)
- local logger = logging.get("gitlinker")
local confs = configs.get()
- logger:ensure(
+ log.ensure(
type(confs._routers[router_type]) == "table",
string.format("unknown router type %s!", vim.inspect(router_type))
)
- logger:ensure(
+ log.ensure(
type(confs._routers[router_type].list_routers) == "table",
string.format("invalid router type %s! 'list_routers' missing.", vim.inspect(router_type))
)
- logger:ensure(
+ log.ensure(
type(confs._routers[router_type].map_routers) == "table",
string.format("invalid router type %s! 'map_routers' missing.", vim.inspect(router_type))
)
@@ -181,7 +176,7 @@ local function _router(router_type, lk)
end
end
end
- logger:ensure(
+ log.ensure(
false,
string.format("%s not support, please bind it in 'router'!", vim.inspect(lk.host))
)
@@ -201,9 +196,9 @@ local function _blame(lk)
end
--- @param opts {action:gitlinker.Action|boolean,router:gitlinker.Router,lstart:integer,lend:integer,message:boolean?,highlight_duration:integer?,remote:string?,file:string?,rev:string?}
+--- @return string?
local _link = function(opts)
local confs = configs.get()
- local logger = logging.get("gitlinker")
-- logger.debug("[link] merged opts: %s", vim.inspect(opts))
local lk = linker.make_linker(opts.remote, opts.file, opts.rev)
@@ -221,7 +216,8 @@ local _link = function(opts)
lk.rev = opts.rev
end
- async.scheduler()
+ async.await(1, vim.schedule)
+
local ok, url = pcall(opts.router, lk, true)
-- logger:debug(
-- "|link| ok:%s, url:%s, router:%s",
@@ -229,7 +225,7 @@ local _link = function(opts)
-- vim.inspect(url),
-- vim.inspect(opts.router)
-- )
- logger:ensure(
+ log.ensure(
ok and str.not_empty(url),
string.format(
"fatal: failed to generate permanent url from remote (%s): %s",
@@ -255,7 +251,7 @@ local _link = function(opts)
if type(opts.message) == "boolean" then
message = opts.message
end
- logger:debug(
+ log.debug(
string.format(
"|_link| message:%s, opts:%s, confs:%s",
vim.inspect(message),
@@ -272,8 +268,11 @@ local _link = function(opts)
return url
end
---- @type fun(opts:{action:gitlinker.Action?,router:gitlinker.Router,lstart:integer,lend:integer,remote:string?,file:string?,rev:string?}):string?
-local _void_link = async.void(_link)
+--- @param opts {action:gitlinker.Action?,router:gitlinker.Router,lstart:integer,lend:integer,remote:string?,file:string?,rev:string?}
+--- @return string?
+local _void_link = function(opts)
+ return async.run(_link, opts)
+end
--- @param args string?
--- @return {router_type:string,remote:string?,file:string?,rev:string?}
@@ -309,12 +308,12 @@ local function setup(opts)
local confs = configs.setup(opts)
-- logger
- logging.setup({
+ log.setup({
name = "gitlinker",
- level = confs.debug and LogLevels.DEBUG or LogLevels.INFO,
- console_log = confs.console_log,
- file_log = confs.file_log,
- file_log_name = "gitlinker.log",
+ level = confs.debug and vim.log.levels.DEBUG or vim.log.levels.INFO,
+ use_console = confs.console_log,
+ use_file = confs.file_log,
+ file_name = "gitlinker.log",
})
-- command
diff --git a/lua/gitlinker/async.lua b/lua/gitlinker/async.lua
deleted file mode 100644
index 59d939f3..00000000
--- a/lua/gitlinker/async.lua
+++ /dev/null
@@ -1,223 +0,0 @@
----@diagnostic disable: luadoc-miss-module-name, undefined-doc-name
---- Small async library for Neovim plugins
---- @module async
--- Store all the async threads in a weak table so we don't prevent them from
--- being garbage collected
-local handles = setmetatable({}, { __mode = "k" })
-local M = {}
--- Note: coroutine.running() was changed between Lua 5.1 and 5.2:
--- - 5.1: Returns the running coroutine, or nil when called by the main thread.
--- - 5.2: Returns the running coroutine plus a boolean, true when the running
--- coroutine is the main one.
---
--- For LuaJIT, 5.2 behaviour is enabled with LUAJIT_ENABLE_LUA52COMPAT
---
--- We need to handle both.
---- Returns whether the current execution context is async.
----
---- @treturn boolean?
-function M.running()
- local current = coroutine.running()
- if current and handles[current] then
- return true
- end
-end
-local function is_Async_T(handle)
- if
- handle
- and type(handle) == "table"
- and vim.is_callable(handle.cancel)
- and vim.is_callable(handle.is_cancelled)
- then
- return true
- end
-end
-local Async_T = {}
--- Analogous to uv.close
-function Async_T:cancel(cb)
- -- Cancel anything running on the event loop
- if self._current and not self._current:is_cancelled() then
- self._current:cancel(cb)
- end
-end
-function Async_T.new(co)
- local handle = setmetatable({}, { __index = Async_T })
- handles[co] = handle
- return handle
-end
--- Analogous to uv.is_closing
-function Async_T:is_cancelled()
- return self._current and self._current:is_cancelled()
-end
---- Run a function in an async context.
---- @tparam function func
---- @tparam function callback
---- @tparam any ... Arguments for func
---- @treturn async_t Handle
-function M.run(func, callback, ...)
- local co = coroutine.create(func)
- local handle = Async_T.new(co)
- local function step(...)
- local ret = { coroutine.resume(co, ...) }
- local ok = ret[1]
- if not ok then
- local err = ret[2]
- error(
- string.format("The coroutine failed with this message:\n%s\n%s", err, debug.traceback(co))
- )
- end
- if coroutine.status(co) == "dead" then
- if callback then
- callback(unpack(ret, 4, table.maxn(ret)))
- end
- return
- end
- local nargs, fn = ret[2], ret[3]
- local args = { select(4, unpack(ret)) }
- assert(type(fn) == "function", "type error :: expected func")
- args[nargs] = step
- local r = fn(unpack(args, 1, nargs))
- if is_Async_T(r) then
- handle._current = r
- end
- end
- step(...)
- return handle
-end
-local function wait(argc, func, ...)
- -- Always run the wrapped functions in xpcall and re-raise the error in the
- -- coroutine. This makes pcall work as normal.
- local function pfunc(...)
- local args = { ... }
- local cb = args[argc]
- args[argc] = function(...)
- cb(true, ...)
- end
- xpcall(func, function(err)
- cb(false, err, debug.traceback())
- end, unpack(args, 1, argc))
- end
- local ret = { coroutine.yield(argc, pfunc, ...) }
- local ok = ret[1]
- if not ok then
- local _, err, traceback = unpack(ret)
- error(string.format("Wrapped function failed: %s\n%s", err, traceback))
- end
- return unpack(ret, 2, table.maxn(ret))
-end
---- Wait on a callback style function
----
---- @tparam integer? argc The number of arguments of func.
---- @tparam function func callback style function to execute
---- @tparam any ... Arguments for func
-function M.wait(...)
- if type(select(1, ...)) == "number" then
- return wait(...)
- end
- -- Assume argc is equal to the number of passed arguments.
- return wait(select("#", ...) - 1, ...)
-end
---- Use this to create a function which executes in an async context but
---- called from a non-async context. Inherently this cannot return anything
---- since it is non-blocking
---- @tparam function func
---- @tparam number argc The number of arguments of func. Defaults to 0
---- @tparam boolean strict Error when called in non-async context
---- @treturn function(...):async_t
-function M.create(func, argc, strict)
- argc = argc or 0
- return function(...)
- if M.running() then
- if strict then
- error("This function must run in a non-async context")
- end
- return func(...)
- end
- local callback = select(argc + 1, ...)
- return M.run(func, callback, unpack({ ... }, 1, argc))
- end
-end
---- Create a function which executes in an async context but
---- called from a non-async context.
---- @tparam function func
---- @tparam boolean strict Error when called in non-async context
-function M.void(func, strict)
- return function(...)
- if M.running() then
- if strict then
- error("This function must run in a non-async context")
- end
- return func(...)
- end
- return M.run(func, nil, ...)
- end
-end
---- Creates an async function with a callback style function.
----
---- @tparam function func A callback style function to be converted. The last argument must be the callback.
---- @tparam integer argc The number of arguments of func. Must be included.
---- @tparam boolean strict Error when called in non-async context
---- @treturn function Returns an async function
-function M.wrap(func, argc, strict)
- return function(...)
- if not M.running() then
- if strict then
- error("This function must run in an async context")
- end
- return func(...)
- end
- return M.wait(argc, func, ...)
- end
-end
---- Run a collection of async functions (`thunks`) concurrently and return when
---- all have finished.
---- @tparam function[] thunks
---- @tparam integer n Max number of thunks to run concurrently
---- @tparam function interrupt_check Function to abort thunks between calls
-function M.join(thunks, n, interrupt_check)
- local function run(finish)
- if #thunks == 0 then
- return finish()
- end
- local remaining = { select(n + 1, unpack(thunks)) }
- local to_go = #thunks
- local ret = {}
- local function cb(...)
- ret[#ret + 1] = { ... }
- to_go = to_go - 1
- if to_go == 0 then
- finish(ret)
- elseif not interrupt_check or not interrupt_check() then
- if #remaining > 0 then
- local next_task = table.remove(remaining)
- next_task(cb)
- end
- end
- end
- for i = 1, math.min(n, #thunks) do
- thunks[i](cb)
- end
- end
- if not M.running() then
- return run
- end
- return M.wait(1, false, run)
-end
---- Partially applying arguments to an async function
---- @tparam function fn
---- @param ... arguments to apply to `fn`
-function M.curry(fn, ...)
- local args = { ... }
- local nargs = select("#", ...)
- return function(...)
- local other = { ... }
- for i = 1, select("#", ...) do
- args[nargs + i] = other[i]
- end
- fn(unpack(args))
- end
-end
---- An async function that when called will yield to the Neovim scheduler to be
---- able to call the neovim API.
-M.scheduler = M.wrap(vim.schedule, 1, false)
-return M
diff --git a/lua/gitlinker/commons/async.lua b/lua/gitlinker/commons/async.lua
index aa1743c9..9185c5e3 100644
--- a/lua/gitlinker/commons/async.lua
+++ b/lua/gitlinker/commons/async.lua
@@ -1,95 +1,1772 @@
--- Copied from:
+---@diagnostic disable
+local util = require('gitlinker.commons.async._util')
-local co = coroutine
+local pack_len = util.pack_len
+local unpack_len = util.unpack_len
+local is_callable = util.is_callable
+local gc_fun = util.gc_fun
-local async_thread = {
- threads = {},
-}
+--- This module implements an asynchronous programming library for Neovim,
+--- centered around the principle of **Structured Concurrency**. This design
+--- makes concurrent programs easier to reason about, more reliable, and less
+--- prone to resource leaks.
+---
+--- ### Core Philosophy: Structured Concurrency
+---
+--- Every async operation happens within a "concurrency scope", which is represented
+--- by a [vim.async.Task] object created with `vim.async.run()`. This creates a
+--- parent-child relationship between tasks, with the following guarantees:
+---
+--- 1. **Task Lifetime:** A parent task's scope cannot end until all of its
+--- child tasks have completed. The parent *implicitly waits* for its children,
+--- preventing orphaned or "fire-and-forget" tasks.
+---
+--- 2. **Error Propagation:** If a child task fails with an error, the error is
+--- propagated up to its parent.
+---
+--- 3. **Cancellation Propagation:** If a parent task is cancelled (e.g., via
+--- `:close()`), the cancellation is propagated down to all of its children.
+---
+--- This model ensures that all concurrent tasks form a clean, hierarchical tree,
+--- and control flow is always well-defined.
+---
+--- ### Stackful vs. Stackless Coroutines (Green Threads)
+---
+--- A key architectural feature of `async.nvim` is that it is built on Lua's
+--- native **stackful coroutines**. This provides a significant advantage over the
+--- `async/await` implementations in many other popular languages, though it's
+--- important to clarify its role in the "function coloring" problem.
+---
+--- - **Stackful (Lua, Go):** A stackful coroutine has its own dedicated call
+--- stack, much like a traditional OS thread (and are often called "green threads"
+--- or "virtual threads"). This allows a coroutine to be suspended from deep
+--- within a nested function call. When using `async.nvim`, `vim.async.run()`
+--- serves as the explicit entry point to an asynchronous context (similar to
+--- Go's `go` keyword). However, *within* that `async.run()` context,
+--- intermediate synchronous helper functions do *not* need to be specially
+--- marked. This means if function `A` calls `B` calls `C`, and `C` performs
+--- an `await`, `A` and `B` can remain regular Lua functions as long as they are
+--- called from within an `async.run()` context. This significantly reduces the
+--- viral spread of "coloring".
+---
+--- - **Stackless (JavaScript, Python, Swift, C#, Kotlin):** Most languages
+--- implement `async/await` with stackless coroutines. A function that can
+--- be suspended must be explicitly marked with a keyword (like `async` or
+--- `suspend`). This requirement is "viral"—any function that calls an `async`
+--- function must itself be marked `async`, and so on up the call stack. This
+--- is the typical "function coloring" problem.
+---
+--- Because Lua provides stackful coroutines, `async.nvim` allows you to `await`
+--- from deeply nested synchronous functions *within an async context* without
+--- "coloring" those intermediate callers. This makes concurrent code less
+--- intrusive and easier to integrate with existing synchronous code, despite
+--- `async.run()` providing an explicit boundary for the async operations.
+---
+--- ### Key Features
+---
+--- - **Task Scopes:** Create a new concurrency scope with `vim.async.run()`.
+--- The returned [vim.async.Task] object acts as the parent for any other
+--- tasks started within its function.
+---
+--- - **Awaiting:** Suspend execution and wait for an operation to complete using
+--- `vim.async.await()`. This can be used on other tasks or on callback-based
+--- functions.
+---
+--- - **Callback Wrapping:** Convert traditional callback-based functions into
+--- modern async functions with `vim.async.wrap()`.
+---
+--- - **Concurrency Utilities:** `await_all`, `await_any`, and `iter` provide
+--- powerful tools for managing groups of tasks.
+---
+--- - **Synchronization Primitives:** `event`, `queue`, and `semaphore` are
+--- available for more complex coordination patterns.
+---
+--- ### Example
+---
+--- ```lua
+--- -- Create an async version of vim.system
+--- local system = vim.async.wrap(3, vim.system)
+---
+--- -- vim.async.run() creates a parent task scope.
+--- local parent_task = vim.async.run(function()
+--- -- These child tasks are launched within the parent's scope.
+--- local ls_task = system({ 'ls', '-l' })
+--- local date_task = system({ 'date' })
+---
+--- -- The parent task will not complete until both ls_task and
+--- -- date_task have finished, even without an explicit 'await'.
+--- end)
+---
+--- -- Wait for the parent and all its children to complete.
+--- parent_task:wait()
+--- ```
+---
+--- ### Structured Concurrency and Task Scopes
+---
+--- Every call to `vim.async.run(fn)` creates a new [vim.async.Task] that establishes
+--- a concurrency scope. Any other tasks started inside `fn` become children of this
+--- task.
+---
+--- ```lua
+--- -- t1 is a top-level task with no parent.
+--- local t1 = async.run(function() vim.async.sleep(50) end)
+---
+--- local main = async.run(function()
+--- -- 'child' is created within main's scope, so 'main' is its parent.
+--- local child = async.run(function() vim.async.sleep(100) end)
+---
+--- -- Because 'main' is the parent, it implicitly waits for 'child'
+--- -- to complete before it can complete itself.
+---
+--- -- Cancellation is also propagated down the tree.
+--- -- Calling main:close() will also call child:close().
+---
+--- -- t1 created outside of the main async context.
+--- -- It has no parent, so 'main' does not implicitly wait for it.
+--- async.await(t1)
+--- end)
+---
+--- -- This will wait for ~100ms, as 'main' must wait for 'child'.
+--- main:wait()
+--- ```
+---
+--- If a parent task finishes with an error, it will immediately cancel all of its
+--- running child tasks. If it finishes normally, it implicitly waits for them to
+--- complete normally.
+---
+--- ### Comparison with Python's Trio
+---
+--- The design of `async.nvim` is heavily inspired by Python's `trio` library,
+--- and it implements the same core philosophy of **Structured Concurrency**.
+--- Both libraries guarantee that all tasks are run in a hierarchy, preventing
+--- leaked or "orphaned" tasks and ensuring that cancellation and errors
+--- propagate predictably.
+---
+--- Trio uses an explicit `nursery` object. To spawn child tasks, you must
+--- create a nursery scope (e.g., `async with trio.open_nursery() as nursery:`),
+--- and the nursery block defines the lifetime of the child tasks.
+---
+--- async.nvim unifies the concepts of a task and a concurrency scope.
+--- The [vim.async.Task] object returned by `vim.async.run()` *is* the scope.
+--- In essence, `async.nvim` provides the same safety and clarity as `trio` but
+--- adapts the concepts idiomatically for Lua and Neovim.
+---
+--- ### Comparison with JavaScript's Promises
+---
+--- JavaScript's `async/await` model with Promises is fundamentally **unstructured**.
+--- While tools like `Promise.all` can coordinate multiple promises, the language
+--- provides no built-in "scope" that automatically manages child tasks.
+---
+--- An `async` function call in JavaScript returns a Promise
+--- that runs independently. If it is not explicitly awaited, it can easily
+--- become an "orphaned" task.
+---
+--- Cancellation is manual and opt-in via the `AbortController`
+--- and `AbortSignal` pattern. It does not automatically propagate from a parent
+--- scope to child operations.
+---
+--- `async.nvim`'s structured model contrasts with this by providing automatic
+--- cleanup and cancellation, preventing common issues like resource leaks from
+--- forgotten background tasks.
+---
+--- ### Comparison with Swift Concurrency
+---
+--- Swift's concurrency model maps closely to `async.nvim`.
+---
+--- Swift's `TaskGroup` is analogous to the concurrency scope
+--- created by `vim.async.run()`. The group's scope cannot exit until all
+--- child tasks added to it are complete.
+---
+--- In both Swift and `async.nvim`, cancelling a parent task
+--- automatically propagates a cancellation notice down to all of its children.
+---
+--- ### Comparison with Kotlin Coroutines
+---
+--- Kotlin's Coroutine framework is another system built on **Structured Concurrency**,
+--- and it shares a nearly identical philosophy with `async.nvim`.
+---
+--- In Kotlin, a `coroutineScope` function creates a new
+--- scope. The scope is guaranteed not to complete until all coroutines
+--- launched within it have also completed. This is conceptually the same as
+--- the scope created by `vim.async.run()`.
+---
+--- Like `async.nvim`, cancellation and errors
+--- propagate automatically through the task hierarchy. Cancelling a parent scope
+--- cancels its children, and an exception in a child will cancel the parent.
+---
+--- ### Comparison with Go Goroutines
+---
+--- Go's concurrency model, while powerful, is fundamentally **unstructured**.
+--- Launching a `go` routine is a "fire-and-forget" operation with no implicit
+--- parent-child relationship.
+---
+--- Programmers must manually track groups of goroutines,
+--- typically using a `sync.WaitGroup` to ensure they all complete before
+--- proceeding.
+---
+--- Cancellation and deadlines are handled by
+--- explicitly passing a `context` object through the entire call stack. There
+--- is no automatic propagation of cancellation or errors up or down a task tree.
+---
+--- This contrasts with `async.nvim`, where the structured concurrency model
+--- automates the lifetime, cancellation, and error management that must be
+--- handled explicitly in Go.
+---
+--- @class vim.async
+local M = {}
-local function threadtostring(x)
- if jit then
- return string.format('%p', x)
- else
- return tostring(x):match('thread: (.*)')
- end
-end
+-- Use max 32-bit signed int value to avoid overflow on 32-bit systems.
+-- Do not use `math.huge` as it is not interpreted as a positive integer on all
+-- platforms.
+local MAX_TIMEOUT = 2 ^ 31 - 1
-function async_thread.running()
- local thread = co.running()
- local id = threadtostring(thread)
- return async_thread.threads[id]
-end
+--- Weak table to keep track of running tasks
+--- @type table?>
+local threads = setmetatable({}, { __mode = 'k' })
-function async_thread.create(fn)
- local thread = co.create(fn)
- local id = threadtostring(thread)
- async_thread.threads[id] = true
- return thread
+--- Returns the currently running task.
+--- @return vim.async.Task?
+local function running()
+ local task = threads[coroutine.running()]
+ if task and not task:completed() then
+ return task
+ end
end
-function async_thread.finished(x)
- if co.status(x) == 'dead' then
- local id = threadtostring(x)
- async_thread.threads[id] = nil
- return true
- end
- return false
+--- Internal marker used to identify that a yielded value is an asynchronous yielding.
+local yield_marker = {}
+local resume_marker = {}
+local complete_marker = {}
+
+local resume_error = 'Unexpected coroutine.resume()'
+local yield_error = 'Unexpected coroutine.yield()'
+
+--- Checks the arguments of a `coroutine.resume`.
+--- This is used to ensure that a resume is expected.
+--- @generic T
+--- @param marker any
+--- @param err? any
+--- @param ... T...
+--- @return T...
+local function check_yield(marker, err, ...)
+ if marker ~= resume_marker then
+ local task = assert(running(), 'Not in async context')
+ task:_raise(resume_error)
+ -- Return an error to the caller. This will also leave the task in a dead
+ -- and unfinshed state
+ error(resume_error, 0)
+ elseif err then
+ error(err, 0)
+ end
+ return ...
end
---- @param async_fn function
---- @param ... any
-local function execute(async_fn, ...)
- local thread = async_thread.create(async_fn)
+--- @class vim.async.Closable
+--- @field close fun(self, callback?: fun())
+--- @field is_closing? fun(self): boolean
+
+--- Tasks are used to run coroutines in event loops. If a coroutine needs to
+--- wait on the event loop, the Task suspends the execution of the coroutine and
+--- waits for event loop to restart it.
+---
+--- Use the [vim.async.run()] to create Tasks.
+---
+--- To close a running Task use the `close()` method. Calling it will cause the
+--- Task to throw a "closed" error in the wrapped coroutine.
+---
+--- Note a Task can be waited on via more than one waiter.
+---
+--- @class vim.async.Task: vim.async.Closable
+--- @field package _thread thread
+--- @field package _future vim.async.Future
+--- @field package _closing boolean
+---
+--- Reference to parent to handle attaching/detaching.
+--- @field package _parent? vim.async.Task
+--- @field package _parent_children_idx? integer
+---
+--- Name of the task
+--- @field name? string
+---
+--- Mark task for internal use. Used for awaiting children tasks on complete.
+--- @field package _internal? string
+---
+--- The source line that created this task, used for inspect().
+--- @field package _caller? string
+---
+--- Maintain children as an array to preserve closure order.
+--- @field package _children table?>
+---
+--- Pointer to last child in children
+--- @field package _children_idx integer
+---
+--- Tasks can await other async functions (task of callback functions)
+--- when we are waiting on a child, we store the handle to it here so we can
+--- close it.
+--- @field package _awaiting? vim.async.Task|vim.async.Closable
+local Task = {}
+
+do --- Task
+ Task.__index = Task
+ --- @package
+ --- @param func function
+ --- @param opts? vim.async.run.Opts
+ --- @return vim.async.Task
+ function Task._new(func, opts)
+ local thread = coroutine.create(function(marker, ...)
+ check_yield(marker)
+ return func(...)
+ end)
+
+ opts = opts or {}
+
+ local self = setmetatable({
+ name = opts.name,
+ _internal = opts._internal,
+ _closing = false,
+ _is_completing = false,
+ _thread = thread,
+ _future = M._future(),
+ _children = {},
+ _children_idx = 0,
+ }, Task)
+
+ threads[thread] = self
+
+ if not (opts and opts.detached) then
+ self:_attach(running())
+ end
+
+ return self
+ end
+
+ -- --- @return boolean
+ -- function Task:closed()
+ -- return self._future._err == 'closed'
+ -- end
+
+ --- @package
+ function Task:_unwait(cb)
+ return self._future:_remove_cb(cb)
+ end
+
+ --- Returns whether the Task has completed.
+ --- @return boolean
+ function Task:completed()
+ return self._future:completed()
+ end
+
+ --- Add a callback to be run when the Task has completed.
+ ---
+ --- - If a timeout or `nil` is provided, the Task will synchronously wait for the
+ --- task to complete for the given time in milliseconds.
+ ---
+ --- ```lua
+ --- local result = task:wait(10) -- wait for 10ms or else error
+ ---
+ --- local result = task:wait() -- wait indefinitely
+ --- ```
+ ---
+ --- - If a function is provided, it will be called when the Task has completed
+ --- with the arguments:
+ --- - (`err: string`) - if the Task completed with an error.
+ --- - (`nil`, `...:any`) - the results of the Task if it completed successfully.
+ ---
+ ---
+ --- If the Task is already done when this method is called, the callback is
+ --- called immediately with the results.
+ --- @param callback_or_timeout integer|fun(err?: any, ...: R...)?
+ --- @overload fun(timeout?: integer): R...
+ function Task:wait(callback_or_timeout)
+ if is_callable(callback_or_timeout) then
+ self._future:wait(callback_or_timeout)
+ return
+ end
+
+ if
+ not vim.wait(callback_or_timeout or MAX_TIMEOUT, function()
+ return self:completed()
+ end)
+ then
+ error('timeout', 2)
+ end
+
+ local res = pack_len(self._future:result())
+
+ assert(self:status() == 'completed' or res[2] == yield_error)
+
+ if not res[1] then
+ error(res[2], 2)
+ end
+
+ return unpack_len(res, 2)
+ end
+
+ --- Protected-call version of `wait()`.
+ ---
+ --- Does not throw an error if the task fails or times out. Instead, returns
+ --- the status and the results.
+ --- @param timeout integer?
+ --- @return boolean, R...
+ function Task:pwait(timeout)
+ vim.validate('timeout', timeout, 'number', true)
+ return pcall(self.wait, self, timeout)
+ end
+
+ --- @package
+ --- @param parent? vim.async.Task
+ function Task:_attach(parent)
+ if parent then
+ -- Attach to parent
+ parent._children_idx = parent._children_idx + 1
+ parent._children[parent._children_idx] = self
+
+ -- Keep track of the parent and this tasks index so we can detach
+ self._parent = parent
+ self._parent_children_idx = parent._children_idx
+ end
+ end
+
+ --- Detach a task from its parent.
+ ---
+ --- The task becomes a top-level task.
+ --- @return vim.async.Task
+ function Task:detach()
+ if self._parent then
+ self._parent._children[self._parent_children_idx] = nil
+ self._parent = nil
+ self._parent_children_idx = nil
+ end
+ return self
+ end
+
+ --- Get the traceback of a task when it is not active.
+ --- Will also get the traceback of nested tasks.
+ ---
+ --- @param msg? string
+ --- @param level? integer
+ --- @return string traceback
+ function Task:traceback(msg, level)
+ level = level or 0
+
+ local thread = ('[%s] '):format(self._thread)
+
+ local awaiting = self._awaiting
+ if getmetatable(awaiting) == Task then
+ --- @cast awaiting vim.async.Task
+ msg = awaiting:traceback(msg, level + 1)
+ end
+
+ local tblvl = getmetatable(awaiting) == Task and 2 or nil
+ msg = (tostring(msg) or '')
+ .. debug.traceback(self._thread, '', tblvl):gsub('\n\t', '\n\t' .. thread)
+
+ if level == 0 then
+ --- @type string
+ msg = msg
+ :gsub('\nstack traceback:\n', '\nSTACK TRACEBACK:\n', 1)
+ :gsub('\nstack traceback:\n', '\n')
+ :gsub('\nSTACK TRACEBACK:\n', '\nstack traceback:\n', 1)
+ end
+
+ return msg
+ end
+
+ --- If a task completes with an error, raise the error
+ --- @return vim.async.Task self
+ function Task:raise_on_error()
+ self:wait(function(err)
+ if err then
+ error(self:traceback(err), 0)
+ end
+ end)
+ return self
+ end
+
+ --- @package
+ --- @param err any
+ function Task:_raise(err)
+ if self:status() == 'running' then
+ -- TODO(lewis6991): is there a better way to do this?
+ vim.schedule(function()
+ self:_resume(err)
+ end)
+ else
+ self:_resume(err)
+ end
+ end
+
+ --- Close the task and all of its children.
+ --- If callback is provided it will run asynchronously,
+ --- else it will run synchronously.
+ ---
+ --- @param callback? fun()
+ function Task:close(callback)
+ if not self:completed() and not self._closing and not self._is_completing then
+ self._closing = true
+ self:_raise('closed')
+ end
+ if callback then
+ self:wait(function()
+ callback()
+ end)
+ end
+ end
+
+ --- Complete a task with the given values, cancelling any remaining work.
+ ---
+ --- This marks the task as successfully completed and notifies any waiters with
+ --- the provided values. It also initiates the cancellation of all
+ --- running child tasks.
+ ---
+ --- A primary use case is for "race" scenarios. A child task can acquire a
+ --- reference to its parent task and call `complete()` on it. This signals
+ --- that the overall goal of the parent scope has been met, which immediately
+ --- triggers the cancellation of all sibling tasks.
+ ---
+ --- This provides a built-in pattern for "first-to-finish" logic, such as
+ --- querying multiple data sources and taking the first response.
+ ---
+ --- @param ... any The values to complete the task with.
+ function Task:complete(...)
+ if self:completed() or self._closing or self._is_completing then
+ error('Task is already completing or completed', 2)
+ end
+ self._is_completing = true
+ self:_raise({ complete_marker, pack_len(...) })
+ end
+
+ --- Checks if an object is closable, i.e., has a `close` method.
+ --- @param obj any
+ --- @return boolean
+ --- @return_cast obj vim.async.Closable
+ local function is_closable(obj)
+ local ty = type(obj)
+ return (ty == 'table' or ty == 'userdata') and is_callable(obj.close)
+ end
+
+ do -- Task:_resume()
+ --- @private
+ --- @param stat boolean
+ --- @param ...R... result
+ function Task:_finalize0(stat, ...)
+ -- TODO(lewis6991): should we collect all errors?
+ for _, child in pairs(self._children) do
+ if not stat then
+ child:close()
+ end
+ -- If child fails then it will resume the parent with an error
+ -- which is handled below
+ pcall(M.await, child)
+ end
+
+ local parent = self._parent
+ self:detach()
- local function step(...)
- local ret = { co.resume(thread, ...) }
- local stat, err_or_fn, nargs = unpack(ret)
+ threads[self._thread] = nil
if not stat then
- error(string.format("The coroutine failed with this message: %s\n%s",
- err_or_fn, debug.traceback(thread)))
+ local err = ...
+ if type(err) == 'table' and err[1] == complete_marker then
+ self._future:complete(nil, unpack_len(err[2]))
+ else
+ local err_msg = err or 'unknown error'
+ self._future:complete(err_msg)
+ if parent and not self._closing then
+ parent:_raise('child error: ' .. tostring(err_msg))
+ end
+ end
+ else
+ self._future:complete(nil, ...)
end
+ end
- if async_thread.finished(thread) then
- return
+ --- @private
+ --- Should only be called in Task:_resume_co()
+ --- @param stat boolean
+ --- @param ...R... result
+ function Task:_finalize(stat, ...)
+ -- Only run self._finalize0() directly if there are children, otherwise
+ -- this will cause infinite recursion:
+ -- M.run() -> task:_resume() -> resume_co() -> complete_task() -> M.run()
+ if next(self._children) ~= nil then
+ -- TODO(lewis6991): should this be detached?
+ M.run({ _internal = true, name = 'await_children' }, self._finalize0, self, stat, ...)
+ else
+ self:_finalize0(stat, ...)
end
+ end
- assert(type(err_or_fn) == "function", "The 1st parameter must be a lua function")
+ --- @param thread thread
+ --- @param on_finish fun(stat: boolean, ...:any)
+ --- @param stat boolean
+ --- @return fun(callback: fun(...:any...): vim.async.Closable?)?
+ local function handle_co_resume(thread, on_finish, stat, ...)
+ if coroutine.status(thread) == 'dead' then
+ on_finish(stat, ...)
+ return
+ end
+
+ local marker, fn = ...
+
+ if marker ~= yield_marker or not is_callable(fn) then
+ on_finish(false, yield_error)
+ return
+ end
+
+ return fn
+ end
+
+ --- @param awaitable fun(callback: fun(...:any...): vim.async.Closable?)
+ --- @param on_defer fun(err?:any, ...:any)
+ --- @return any[]? next_args
+ --- @return vim.async.Closable? closable
+ local function handle_awaitable(awaitable, on_defer)
+ local ok, closable_or_err
+ local settled = false
+ local next_args --- @type any[]?
+ ok, closable_or_err = pcall(awaitable, function(...)
+ if settled then
+ -- error here?
+ return
+ end
+ settled = true
+
+ if ok == nil then
+ next_args = pack_len(...)
+ else
+ on_defer(...)
+ end
+ end)
+
+ if not ok then
+ return pack_len(closable_or_err)
+ elseif is_closable(closable_or_err) then
+ return next_args, closable_or_err
+ else
+ return next_args
+ end
+ end
+
+ --- @param task vim.async.Task
+ --- @param awaiting vim.async.Task|vim.async.Closable
+ --- @return boolean
+ local function can_close_awaiting(task, awaiting)
+ if getmetatable(awaiting) ~= Task then
+ return true
+ end
+
+ for _, child in pairs(task._children) do
+ if child == awaiting then
+ return true
+ end
+ end
+
+ return false
+ end
+
+ --- Handle closing an awaitable if needed
+ --- @param task vim.async.Task
+ --- @param awaiting vim.async.Closable?
+ --- @param on_continue fun()
+ --- @return boolean should_return
+ --- @return {[integer]: any, n: integer}? new_args
+ local function handle_close_awaiting(task, awaiting, on_continue)
+ if not awaiting or not can_close_awaiting(task, awaiting) then
+ return false, nil
+ end
+
+ -- Check if the awaitable is already closing (if it has an is_closing method)
+ local already_closing = false
+ if type(awaiting.is_closing) == 'function' then
+ already_closing = awaiting:is_closing()
+ end
+
+ if already_closing then
+ -- Already closing, just continue without calling close
+ task._awaiting = nil
+ on_continue()
+ return true, nil
+ end
- local ret_fn = err_or_fn
- local args = { select(4, unpack(ret)) }
- args[nargs] = step
- ret_fn(unpack(args, 1, nargs --[[@as integer]]))
- end
+ -- We must close the closable child before we resume to ensure
+ -- all resources are collected.
+ --- @diagnostic disable-next-line: param-type-not-match
+ local close_ok, close_err = pcall(awaiting.close, awaiting, function()
+ task._awaiting = nil
+ on_continue()
+ end)
- step(...)
+ if close_ok then
+ -- will call on_continue in close callback
+ return true, nil
+ end
+
+ -- Close failed (synchronously) raise error
+ return false, pack_len(close_err)
+ end
+
+ --- @package
+ --- @param ... any the first argument is the error, except for when the coroutine begins
+ function Task:_resume(...)
+ --- @type {[integer]: any, n: integer}?
+ local args = pack_len(...)
+
+ -- Run this block in a while loop to run non-deferred continuations
+ -- without a new stack frame.
+ while args do
+ -- TODO(lewis6991): Add a test that handles awaiting in the non-deferred
+ -- continuation
+ if self._is_completing and select(1, ...) == 'closed' then
+ return
+ end
+
+ local should_return, close_err_args = handle_close_awaiting(self, self._awaiting, function()
+ self:_resume(unpack_len(args))
+ end)
+ if should_return then
+ return
+ end
+
+ args = close_err_args or args
+
+ -- Check the coroutine is still alive before trying to resume it
+ if coroutine.status(self._thread) == 'dead' then
+ -- Can only happen if coroutine.resume() is called outside of this
+ -- function. When that happens check_yield() will error the coroutine
+ -- which puts it in the 'dead' state.
+ self:_finalize(false, (...))
+ return
+ end
+
+ -- Level-triggered cancellation: if the task is closing and the coroutine
+ -- completed successfully (e.g., after pcall caught the cancellation and
+ -- then did another await), override the success with "closed" error.
+ -- This ensures cancellations persist across pcall catches.
+ local awaitable = handle_co_resume(self._thread, function(stat2, ...)
+ if self._closing and stat2 then
+ self:_finalize(false, 'closed')
+ else
+ self:_finalize(stat2, ...)
+ end
+ end, coroutine.resume(self._thread, resume_marker, unpack_len(args)))
+
+ if not awaitable then
+ return
+ end
+
+ args, self._awaiting = handle_awaitable(awaitable, function(...)
+ if not self:completed() then
+ self:_resume(...)
+ end
+ end)
+ end
+ end
+ end
+
+ --- @package
+ function Task:_log(...)
+ print(tostring(self._thread), ...)
+ end
+
+ --- Returns the status of the task:
+ --- - 'running' : task is running (that is, is called `status()`).
+ --- - 'normal' : task is active but not running (e.g. it is starting
+ --- another task).
+ --- - 'awaiting' : if the task is awaiting another task either directly via
+ --- `await()` or waiting for all children to complete.
+ --- - 'completed' : task and all it's children have completed
+ --- @return 'running'|'awaiting'|'normal'|'scheduled'|'completed'
+ function Task:status()
+ local co_status = coroutine.status(self._thread)
+ if co_status == 'dead' then
+ return self:completed() and 'completed' or 'awaiting'
+ elseif co_status == 'suspended' then
+ return 'awaiting'
+ elseif co_status == 'normal' then
+ -- TODO(lewis6991): This state is a bit ambiguous. If all tasks
+ -- are started from the main thread, then we can remove this state.
+ -- Though it still may be possible if the user resumes a non-task
+ -- coroutine.
+ return 'normal'
+ end
+ assert(co_status == 'running')
+ return 'running'
+ end
end
-local M = {}
+do --- M.run
+ --- @class vim.async.run.Opts
+ --- @field name? string
+ --- @field detached? boolean
+ --- @field package _internal? boolean
+
+ --- @package
+ --- @generic T, R
+ --- @param opts? vim.async.run.Opts
+ --- @param func async fun(...:T...): R... Function to run in an async context
+ --- @param ... T... Arguments to pass to the function
+ --- @return vim.async.Task
+ local function run(opts, func, ...)
+ vim.validate('opts', opts, 'table', true)
+ vim.validate('func', func, 'callable')
+ -- TODO(lewis6991): add task names
+ local task = Task._new(func, opts)
+ local info = debug.getinfo(2, 'Sl')
+ if info and info.currentline then
+ task._caller = ('%s:%d'):format(info.source, info.currentline)
+ end
+ task:_resume(...)
+ return task
+ end
---- @param func function
+ --- Run a function in an async context, asynchronously.
+ ---
+ --- Returns an [vim.async.Task] object which can be used to wait or await the result
+ --- of the function.
+ ---
+ --- Examples:
+ --- ```lua
+ --- -- Run a uv function and wait for it
+ --- local stat = vim.async.run(function()
+ --- return vim.async.await(2, vim.uv.fs_stat, 'foo.txt')
+ --- end):wait()
+ ---
+ --- -- Since uv functions have sync versions, this is the same as:
+ --- local stat = vim.fs_stat('foo.txt')
+ --- ```
+ --- @generic T, R
+ --- @param func async fun(...:T...): R...
+ --- @return vim.async.Task
+ --- @overload fun(name: string, func: async fun(...:T...), ...: T...): vim.async.Task
+ --- @overload fun(opts: vim.async.run.Opts, func: async fun(...:T...), ...: T...): vim.async.Task
+ function M.run(func, ...)
+ if type(func) == 'string' then
+ return run({ name = func }, ...)
+ elseif type(func) == 'table' then
+ return run(func, ...)
+ elseif is_callable(func) then
+ return run(nil, func, ...)
+ end
+ error('Invalid arguments')
+ end
+end
+
+do --- M.await()
+ --- @generic T, R
+ --- @param argc integer
+ --- @param fun fun(...: T..., callback: fun(...: R...))
+ --- @param ... T... func arguments
+ --- @return fun(callback: fun(...: R...))
+ local function norm_cb_fun(argc, fun, ...)
+ local args = pack_len(...)
+
+ --- @param callback fun(...:any)
+ --- @return any?
+ return function(callback)
+ args[argc] = function(...)
+ callback(nil, ...)
+ end
+ args.n = math.max(args.n, argc)
+ return fun(unpack_len(args))
+ end
+ end
+
+ --- Asynchronous blocking wait
+ ---
+ --- Example:
+ --- ```lua
+ --- local task = vim.async.run(function()
+ --- return 1, 'a'
+ --- end)
+ ---
+ --- local task_fun = vim.async.async(function(arg)
+ --- return 2, 'b', arg
+ --- end)
+ ---
+ --- vim.async.run(function()
+ --- do -- await a callback function
+ --- vim.async.await(1, vim.schedule)
+ --- end
+ ---
+ --- do -- await a callback function (if function only has a callback argument)
+ --- vim.async.await(vim.schedule)
+ --- end
+ ---
+ --- do -- await a task (new async context)
+ --- local n, s = vim.async.await(task)
+ --- assert(n == 1 and s == 'a')
+ --- end
+ ---
+ --- end)
+ --- ```
+ --- @async
+ --- @generic T, R
+ --- @param ... any see overloads
+ --- @overload async fun(func: (fun(callback: fun(...:R...)): vim.async.Closable?)): R...
+ --- @overload async fun(argc: integer, func: (fun(...:T..., callback: fun(...:R...)): vim.async.Closable?), ...:T...): R...
+ --- @overload async fun(task: vim.async.Task): R...
+ function M.await(...)
+ local task = running()
+ assert(task, 'Not in async context')
+
+ -- TODO(lewis6991): needs test coverage. Happens when a task pcalls an await
+ if task._closing then
+ error('closed', 0)
+ end
+
+ local arg1 = select(1, ...)
+
+ local fn --- @type fun(...:R...): vim.async.Closable?
+ if type(arg1) == 'number' then
+ fn = norm_cb_fun(...)
+ elseif type(arg1) == 'function' then
+ fn = norm_cb_fun(1, arg1)
+ elseif getmetatable(arg1) == Task then
+ fn = function(callback)
+ --- @cast arg1 vim.async.Task
+ arg1:wait(callback)
+ return arg1
+ end
+ else
+ error('Invalid arguments, expected Task or (argc, func) got: ' .. vim.inspect(arg1), 2)
+ end
+
+ return check_yield(coroutine.yield(yield_marker, fn))
+ end
+end
+
+--- Returns true if the current task has been closed.
+---
+--- Can be used in an async function to do cleanup when a task is closing.
+--- @return boolean
+function M.is_closing()
+ local task = running()
+ return task and task._closing or false
+end
+
+--- Protected call for async functions that propagates child task errors.
+---
+--- Similar to Lua's built-in `pcall`, but with special handling for child task
+--- errors. This function will:
+--- - Catch regular errors (return `false, err`)
+--- - Propagate child task errors (re-throw them)
+--- - Propagate cancellation errors (re-throw them)
+---
+--- This is useful when you want to handle regular errors locally, but allow
+--- child task failures to bubble up to the parent task, maintaining the
+--- structured concurrency guarantees.
+---
+--- Note: This function uses `xpcall` with a custom error handler to capture the
+--- full stack trace before the stack is unwound. When re-throwing child errors
+--- or cancellation errors, the traceback is preserved. Regular errors are
+--- caught and returned with their error messages as usual.
+---
+--- Example:
+--- ```lua
+--- vim.async.run(function()
+--- local child = vim.async.run(function()
+--- vim.async.sleep(10)
+--- error('CHILD_FAILED')
+--- end)
+---
+--- -- Regular pcall would catch the child error
+--- local ok1, err1 = pcall(function()
+--- vim.async.sleep(100)
+--- end)
+--- -- ok1 = false, err1 = 'child error: CHILD_FAILED'
+---
+--- -- async.pcall propagates child errors
+--- local ok2, err2 = vim.async.pcall(function()
+--- error('REGULAR_ERROR')
+--- end)
+--- -- ok2 = false, err2 = 'REGULAR_ERROR'
+---
+--- -- But child errors propagate:
+--- vim.async.pcall(function()
+--- vim.async.sleep(100)
+--- end)
+--- -- This will error with 'child error: CHILD_FAILED'
+--- end)
+--- ```
+---
+--- @async
+--- @generic T
+--- @param fn async fun(): T...
+--- @return boolean ok
+--- @return any|T... err_or_result
+function M.pcall(fn)
+ vim.validate('fn', fn, 'callable')
+
+ local captured_traceback
+ local results = pack_len(xpcall(fn, function(err)
+ -- Error handler runs before stack is unwound
+ -- Capture the full traceback here
+ captured_traceback = debug.traceback(err, 2)
+
+ -- Check if this is a child error or cancellation
+ if type(err) == 'string' then
+ if err:match('^child error: ') or err == 'closed' then
+ -- For child errors/cancellations, return the traceback so it can be re-thrown
+ return captured_traceback
+ end
+ end
+
+ -- For regular errors, just return the error message
+ return err
+ end))
+
+ local ok = results[1]
+
+ if not ok then
+ local err = results[2]
+ -- If this is a child error or cancellation, re-throw with the full traceback
+ if err == captured_traceback then
+ -- This is a child error or cancellation - re-throw it
+ error(err, 0)
+ end
+ end
+
+ return unpack_len(results)
+end
+
+--- Creates an async function from a callback style function.
+---
+--- `func` can optionally return an object with a close method to clean up
+--- resources. Note this method will be called when the task finishes or
+--- interrupted.
+---
+--- Example:
+---
+--- ```lua
+--- --- Note the callback argument is not present in the return function
+--- --- @type async fun(timeout: integer)
+--- local sleep = vim.async.wrap(2, function(timeout, callback)
+--- local timer = vim.uv.new_timer()
+--- timer:start(timeout * 1000, 0, callback)
+--- -- uv_timer_t provides a close method so timer will be
+--- -- cleaned up when this function finishes
+--- return timer
+--- end)
+---
+--- vim.async.run(function()
+--- print('hello')
+--- sleep(2)
+--- print('world')
+--- end)
+--- ```
+---
+--- @generic T, R
--- @param argc integer
---- @return function
-M.wrap = function(func, argc)
- return function(...)
- if not async_thread.running() then
- return func(...)
+--- @param func fun(...: T..., callback: fun(...: R...)): vim.async.Closable?
+--- @return async fun(...:T...): R...
+function M.wrap(argc, func)
+ vim.validate('argc', argc, 'number')
+ vim.validate('func', func, 'callable')
+ --- @async
+ return function(...)
+ return M.await(argc, func, ...)
+ end
+end
+
+do --- M.iter(), M.await_all(), M.await_any()
+ --- @async
+ --- @generic R
+ --- @param tasks vim.async.Task[] A list of tasks to wait for and iterate over.
+ --- @return async fun(): (integer?, any?, ...R) iterator that yields the index, error, and results of each task.
+ local function iter(tasks)
+ vim.validate('tasks', tasks, 'table')
+
+ -- TODO(lewis6991): do not return err, instead raise any errors as they occur
+ assert(running(), 'Not in async context')
+
+ local remaining = #tasks
+ local queue = M._queue()
+
+ -- Keep track of the callbacks so we can remove them when the iterator
+ -- is garbage collected.
+ --- @type table,function>
+ local task_cbs = setmetatable({}, { __mode = 'v' })
+
+ -- Wait on all the tasks. Keep references to the task futures and wait
+ -- callbacks so we can remove them when the iterator is garbage collected.
+ for i, task in ipairs(tasks) do
+ local function cb(err, ...)
+ remaining = remaining - 1
+ queue:put_nowait(pack_len(err, i, ...))
+ if remaining == 0 then
+ queue:put_nowait()
+ end
+ end
+
+ task_cbs[task] = cb
+ task:wait(cb)
+ end
+
+ --- @async
+ return gc_fun(function()
+ local r = queue:get()
+ if r then
+ local err = r[1]
+ if err then
+ -- -- Note: if the task was a child, then an error should have already been
+ -- -- raised in _complete_task(). This should only trigger to detached tasks.
+ -- assert(assert(tasks[r[2]])._parent == nil)
+ error(('iter error[index:%d]: %s'):format(r[2], r[1]), 3)
+ end
+ return unpack_len(r, 2)
+ end
+ end, function()
+ for t, tcb in pairs(task_cbs) do
+ t:_unwait(tcb)
+ end
+ end)
+ end
+
+ --- Waits for multiple tasks to finish and iterates over their results.
+ ---
+ --- This function allows you to run multiple asynchronous tasks concurrently and
+ --- process their results as they complete. It returns an iterator function that
+ --- yields the index of the task, any error encountered, and the results of the
+ --- task.
+ ---
+ --- If a task completes with an error, the error is returned as the second
+ --- value. Otherwise, the results of the task are returned as subsequent values.
+ ---
+ --- Example:
+ --- ```lua
+ --- local task1 = vim.async.run(function()
+ --- return 1, 'a'
+ --- end)
+ ---
+ --- local task2 = vim.async.run(function()
+ --- return 2, 'b'
+ --- end)
+ ---
+ --- local task3 = vim.async.run(function()
+ --- error('task3 error')
+ --- end)
+ ---
+ --- vim.async.run(function()
+ --- for i, err, r1, r2 in vim.async.iter({task1, task2, task3}) do
+ --- print(i, err, r1, r2)
+ --- end
+ --- end)
+ --- ```
+ ---
+ --- Prints:
+ --- ```
+ --- 1 nil 1 'a'
+ --- 2 nil 2 'b'
+ --- 3 'task3 error' nil nil
+ --- ```
+ ---
+ --- @async
+ --- @generic R
+ --- @param tasks vim.async.Task[] A list of tasks to wait for and iterate over.
+ --- @return async fun(): (integer?, any?, ...R) iterator that yields the index, error, and results of each task.
+ function M.iter(tasks)
+ return iter(tasks)
+ end
+
+ --- Wait for all tasks to finish and return their results.
+ ---
+ --- Example:
+ --- ```lua
+ --- local task1 = vim.async.run(function()
+ --- return 1, 'a'
+ --- end)
+ ---
+ --- local task2 = vim.async.run(function()
+ --- return 1, 'a'
+ --- end)
+ ---
+ --- local task3 = vim.async.run(function()
+ --- error('task3 error')
+ --- end)
+ ---
+ --- vim.async.run(function()
+ --- local results = vim.async.await_all({task1, task2, task3})
+ --- print(vim.inspect(results))
+ --- end)
+ --- ```
+ ---
+ --- Prints:
+ --- ```
+ --- {
+ --- [1] = { nil, 1, 'a' },
+ --- [2] = { nil, 2, 'b' },
+ --- [3] = { 'task2 error' },
+ --- }
+ --- ```
+ --- @async
+ --- @param tasks vim.async.Task[]
+ --- @return table
+ function M.await_all(tasks)
+ assert(running(), 'Not in async context')
+ local itr = iter(tasks)
+ local results = {} --- @type table
+
+ local function collect(i, ...)
+ if i then
+ results[i] = pack_len(...)
+ end
+ return i ~= nil
+ end
+
+ while collect(itr()) do
+ end
+
+ return results
+ end
+
+ --- Wait for the first task to complete and return its result.
+ ---
+ --- Example:
+ --- ```lua
+ --- local task1 = vim.async.run(function()
+ --- vim.async.sleep(100)
+ --- return 1, 'a'
+ --- end)
+ ---
+ --- local task2 = vim.async.run(function()
+ --- return 2, 'b'
+ --- end)
+ ---
+ --- vim.async.run(function()
+ --- local i, err, r1, r2 = vim.async.await_any({task1, task2})
+ --- assert(i == 2)
+ --- assert(err == nil)
+ --- assert(r1 == 2)
+ --- assert(r2 == 'b')
+ --- end)
+ --- ```
+ --- @async
+ --- @param tasks vim.async.Task[]
+ --- @return integer? index
+ --- @return any? err
+ --- @return any ... results
+ function M.await_any(tasks)
+ return iter(tasks)()
+ end
+end
+
+--- Asynchronously sleep for a given duration.
+---
+--- Blocks the current task for the given duration, but does not block the main
+--- thread.
+--- @async
+--- @param duration integer ms
+function M.sleep(duration)
+ vim.validate('duration', duration, 'number')
+ M.await(1, function(callback)
+ -- TODO(lewis6991): should return the result of defer_fn here.
+ vim.defer_fn(callback, duration)
+ end)
+end
+
+--- Run a task with a timeout.
+---
+--- If the task does not complete within the specified duration, it is closed
+--- and an error is thrown.
+--- @async
+--- @generic R
+--- @param duration integer Timeout duration in milliseconds
+--- @param task vim.async.Task
+--- @return R
+function M.timeout(duration, task)
+ vim.validate('duration', duration, 'number')
+ vim.validate('task', task, 'table')
+ local timer = M.run(M.await, function(callback)
+ local t = assert(vim.uv.new_timer())
+ t:start(duration, 0, callback)
+ return t
+ end)
+ if M.await_any({ task, timer }) == 2 then
+ -- Timer completed first, close the task
+ task:close()
+ error('timeout')
+ end
+ return M.await(task)
+end
+
+do --- M._future()
+ --- Future objects are used to bridge low-level callback-based code with
+ --- high-level async/await code.
+ --- @class vim.async.Future
+ --- @field private _callbacks table
+ --- @field private _callback_pos integer
+ --- Error result of the task is an error occurs.
+ --- Must use `await` to get the result.
+ --- @field package _err? any
+ ---
+ --- Result of the task.
+ --- Must use `await` to get the result.
+ --- @field private _result? R[]
+ local Future = {}
+ Future.__index = Future
+
+ --- Return `true` if the Future is completed.
+ --- @return boolean
+ function Future:completed()
+ return (self._err or self._result) ~= nil
+ end
+
+ --- Return the result of the Future.
+ ---
+ --- If the Future is done and has a result set by the `complete()` method, the
+ --- result is returned.
+ ---
+ --- If the Future’s result isn’t yet available, this method raises a
+ --- "Future has not completed" error.
+ --- @return boolean stat true if the Future completed successfully, false otherwise.
+ --- @return any ... error or result
+ function Future:result()
+ if not self:completed() then
+ error('Future has not completed', 2)
+ end
+ if self._err then
+ return false, self._err
+ else
+ return true, unpack_len(self._result)
+ end
+ end
+
+ --- Add a callback to be run when the Future is done.
+ ---
+ --- The callback is called with the arguments:
+ --- - (`err: string`) - if the Future completed with an error.
+ --- - (`nil`, `...:any`) - the results of the Future if it completed successfully.
+ ---
+ --- If the Future is already done when this method is called, the callback is
+ --- called immediately with the results.
+ --- @param callback fun(err?: any, ...: any)
+ function Future:wait(callback)
+ if self:completed() then
+ -- Already completed or closed
+ callback(self._err, unpack_len(self._result))
+ else
+ self._callbacks[self._callback_pos] = callback
+ self._callback_pos = self._callback_pos + 1
+ end
+ end
+
+ --- Mark the Future as complete and set its result.
+ ---
+ --- If an error is provided, the Future is marked as failed. Otherwise, it is
+ --- marked as successful with the provided result.
+ ---
+ --- This will trigger any callbacks that are waiting on the Future.
+ --- @param err? any
+ --- @param ... any result
+ function Future:complete(err, ...)
+ if err ~= nil then
+ self._err = err
+ else
+ self._result = pack_len(...)
+ end
+
+ local errs = {} --- @type string[]
+ -- Need to use pairs to avoid gaps caused by removed callbacks
+ for _, cb in pairs(self._callbacks) do
+ local ok, cb_err = pcall(cb, err, ...)
+ if not ok then
+ errs[#errs + 1] = cb_err
+ end
+ end
+
+ if #errs > 0 then
+ error(table.concat(errs, '\n'), 0)
+ end
+ end
+
+ --- @package
+ --- Removes a callback from the Future.
+ --- @param cb fun(err?: any, ...: any)
+ function Future:_remove_cb(cb)
+ for j, fcb in pairs(self._callbacks) do
+ if fcb == cb then
+ self._callbacks[j] = nil
+ break
end
- return co.yield(func, argc, ...)
- end
+ end
+ end
+
+ --- @package
+ --- Create a new future.
+ ---
+ --- A Future is a low-level awaitable that is not intended to be used in
+ --- application-level code.
+ --- @return vim.async.Future
+ function M._future()
+ return setmetatable({
+ _callbacks = {},
+ _callback_pos = 1,
+ }, Future)
+ end
end
---- @param func function
---- @return function
-M.void = function(func)
- return function(...)
- if async_thread.running() then
- return func(...)
+do --- M._event()
+ --- An event can be used to notify multiple tasks that some event has
+ --- happened. An Event object manages an internal flag that can be set to true
+ --- with the `set()` method and reset to `false` with the `clear()` method.
+ --- The `wait()` method blocks until the flag is set to `true`. The flag is
+ --- set to `false` initially.
+ --- @class vim.async.Event
+ --- @field private _is_set boolean
+ --- @field private _waiters function[]
+ local Event = {}
+ Event.__index = Event
+
+ --- Set the event.
+ ---
+ --- All tasks waiting for event to be set will be immediately awakened.
+ ---
+ --- If `max_woken` is provided, only up to `max_woken` waiters will be woken.
+ --- The event will be reset to `false` if there are more waiters remaining.
+ --- @param max_woken? integer
+ function Event:set(max_woken)
+ if self._is_set then
+ return
+ end
+ self._is_set = true
+ local waiters = self._waiters
+ local waiters_to_notify = {} --- @type function[]
+ max_woken = max_woken or #waiters
+ while #waiters > 0 and #waiters_to_notify < max_woken do
+ waiters_to_notify[#waiters_to_notify + 1] = table.remove(waiters, 1)
+ end
+ if #waiters > 0 then
+ self._is_set = false
+ end
+ for _, waiter in ipairs(waiters_to_notify) do
+ waiter()
+ end
+ end
+
+ --- Wait until the event is set.
+ ---
+ --- If the event is set, return immediately. Otherwise block until another
+ --- task calls set().
+ --- @async
+ function Event:wait()
+ M.await(function(callback)
+ if self._is_set then
+ callback()
+ else
+ table.insert(self._waiters, callback)
end
- execute(func, ...)
- end
+ end)
+ end
+
+ --- Clear (unset) the event.
+ ---
+ --- Tasks awaiting on wait() will now block until the set() method is called
+ --- again.
+ function Event:clear()
+ self._is_set = false
+ end
+
+ --- @package
+ --- Create a new event.
+ ---
+ --- An event can signal to multiple listeners to resume execution
+ --- The event can be set from a non-async context.
+ ---
+ --- ```lua
+ --- local event = vim.async._event()
+ ---
+ --- local worker = vim.async.run(function()
+ --- vim.async.sleep(1000)
+ --- event.set()
+ --- end)
+ ---
+ --- local listeners = {
+ --- vim.async.run(function()
+ --- event:wait()
+ --- print("First listener notified")
+ --- end),
+ --- vim.async.run(function()
+ --- event:wait()
+ --- print("Second listener notified")
+ --- end),
+ --- }
+ --- ```
+ --- @return vim.async.Event
+ function M._event()
+ return setmetatable({
+ _waiters = {},
+ _is_set = false,
+ }, Event)
+ end
end
-M.schedule = M.wrap(vim.schedule, 1)
+do --- M._queue()
+ --- @class vim.async.Queue
+ --- @field private _non_empty vim.async.Event
+ --- @field package _non_full vim.async.Event
+ --- @field private _max_size? integer
+ --- @field private _items R[]
+ --- @field private _right_i integer
+ --- @field private _left_i integer
+ local Queue = {}
+ Queue.__index = Queue
+
+ --- Returns the number of items in the queue.
+ --- @return integer
+ function Queue:size()
+ return self._right_i - self._left_i
+ end
+
+ --- Returns the maximum number of items in the queue.
+ --- @return integer?
+ function Queue:max_size()
+ return self._max_size
+ end
+
+ --- Put an item into the queue.
+ ---
+ --- If the queue is full, wait until a free slot is available.
+ --- @async
+ --- @param value any
+ function Queue:put(value)
+ self._non_full:wait()
+ self:put_nowait(value)
+ end
+
+ --- Get an item from the queue.
+ ---
+ --- If the queue is empty, wait until an item is available.
+ --- @async
+ --- @return any
+ function Queue:get()
+ self._non_empty:wait()
+ return self:get_nowait()
+ end
+
+ --- Get an item from the queue without blocking.
+ ---
+ --- If the queue is empty, raise an error.
+ --- @return any
+ function Queue:get_nowait()
+ if self:size() == 0 then
+ error('Queue is empty', 2)
+ end
+ -- TODO(lewis6991): For a long_running queue, _left_i might overflow.
+ self._left_i = self._left_i + 1
+ local item = self._items[self._left_i]
+ self._items[self._left_i] = nil
+ if self._left_i == self._right_i then
+ self._non_empty:clear()
+ end
+ self._non_full:set(1)
+ return item
+ end
+
+ --- Put an item into the queue without blocking.
+ --- If no free slot is immediately available, raise "Queue is full" error.
+ --- @param value any
+ function Queue:put_nowait(value)
+ if self:size() == self:max_size() then
+ error('Queue is full', 2)
+ end
+ self._right_i = self._right_i + 1
+ self._items[self._right_i] = value
+ self._non_empty:set(1)
+ if self:size() == self.max_size then
+ self._non_full:clear()
+ end
+ end
+
+ --- @package
+ --- Create a new FIFO queue with async support.
+ --- ```lua
+ --- local queue = vim.async._queue()
+ ---
+ --- local producer = vim.async.run(function()
+ --- for i = 1, 10 do
+ --- vim.async.sleep(100)
+ --- queue:put(i)
+ --- end
+ --- queue:put(nil)
+ --- end)
+ ---
+ --- vim.async.run(function()
+ --- while true do
+ --- local value = queue:get()
+ --- if value == nil then
+ --- break
+ --- end
+ --- print(value)
+ --- end
+ --- print("Done")
+ --- end)
+ --- ```
+ --- @param max_size? integer The maximum number of items in the queue, defaults to no limit
+ --- @return vim.async.Queue
+ function M._queue(max_size)
+ local self = setmetatable({
+ _items = {},
+ _left_i = 0,
+ _right_i = 0,
+ _max_size = max_size,
+ _non_empty = M._event(),
+ _non_full = M._event(),
+ }, Queue)
+
+ self._non_full:set()
+
+ return self
+ end
+end
+
+do --- M.semaphore()
+ --- A semaphore manages an internal counter which is decremented by each
+ --- `acquire()` call and incremented by each `release()` call. The counter can
+ --- never go below zero; when `acquire()` finds that it is zero, it blocks,
+ --- waiting until some task calls `release()`.
+ ---
+ --- The preferred way to use a Semaphore is with the `with()` method, which
+ --- automatically acquires and releases the semaphore around a function call.
+ --- @class vim.async.Semaphore
+ --- @field private _permits integer
+ --- @field private _max_permits integer
+ --- @field package _event vim.async.Event
+ local Semaphore = {}
+ Semaphore.__index = Semaphore
+
+ --- Executes a function within the semaphore.
+ ---
+ --- This acquires the semaphore before running the function and releases it
+ --- after the function completes, even if it errors.
+ --- @async
+ --- @generic R
+ --- @param fn async fun(): R... # Function to execute within the semaphore's context.
+ --- @return R... # Result(s) of the executed function.
+ function Semaphore:with(fn)
+ self:acquire()
+ local r = pack_len(pcall(fn))
+ self:release()
+ local stat = r[1]
+ if not stat then
+ local err = r[2]
+ error(err)
+ end
+ return unpack_len(r, 2)
+ end
+
+ --- Acquire a semaphore.
+ ---
+ --- If the internal counter is greater than zero, decrement it by `1` and
+ --- return immediately. If it is `0`, wait until a `release()` is called.
+ --- @async
+ function Semaphore:acquire()
+ self._event:wait()
+ self._permits = self._permits - 1
+ assert(self._permits >= 0, 'Semaphore value is negative')
+ if self._permits == 0 then
+ self._event:clear()
+ end
+ end
+
+ --- Release a semaphore.
+ ---
+ --- Increments the internal counter by `1`. Can wake
+ --- up a task waiting to acquire the semaphore.
+ function Semaphore:release()
+ if self._permits >= self._max_permits then
+ error('Semaphore value is greater than max permits', 2)
+ end
+ self._permits = self._permits + 1
+ self._event:set(1)
+ end
+
+ --- Create an async semaphore that allows up to a given number of acquisitions.
+ ---
+ --- ```lua
+ --- vim.async.run(function()
+ --- local semaphore = vim.async.semaphore(2)
+ ---
+ --- local tasks = {}
+ ---
+ --- local value = 0
+ --- for i = 1, 10 do
+ --- tasks[i] = vim.async.run(function()
+ --- semaphore:with(function()
+ --- value = value + 1
+ --- vim.async.sleep(10)
+ --- print(value) -- Never more than 2
+ --- value = value - 1
+ --- end)
+ --- end)
+ --- end
+ ---
+ --- vim.async.await_all(tasks)
+ --- assert(value <= 2)
+ --- end)
+ --- ```
+ --- @param permits? integer (default: 1)
+ --- @return vim.async.Semaphore
+ function M.semaphore(permits)
+ vim.validate('permits', permits, 'number', true)
+ permits = permits or 1
+ local obj = setmetatable({
+ _max_permits = permits,
+ _permits = permits,
+ _event = M._event(),
+ }, Semaphore)
+ obj._event:set()
+ return obj
+ end
+end
+
+do --- M._inspect_tree()
+ --- @private
+ --- @param parent? vim.async.Task
+ --- @param prefix? string
+ --- @return string[]
+ local function inspect(parent, prefix)
+ local tasks = {} --- @type table?>
+ if parent then
+ for _, task in pairs(parent._children) do
+ if not task._internal then
+ tasks[#tasks + 1] = task
+ end
+ end
+ else
+ -- Gather for all detached tasks
+ for _, task in pairs(threads) do
+ if not task._parent and not task._internal then
+ tasks[#tasks + 1] = task
+ end
+ end
+ end
+
+ local r = {} --- @type string[]
+ for i, task in ipairs(tasks) do
+ local last = i == #tasks
+ r[#r + 1] = ('%s%s%s%s [%s]'):format(
+ prefix or '',
+ parent and (last and '└─ ' or '├─ ') or '',
+ task.name or '',
+ task._caller,
+ task:status()
+ )
+ local child_prefix = (prefix or '') .. (parent and (last and ' ' or '│ ') or '')
+ vim.list_extend(r, inspect(task, child_prefix))
+ end
+ return r
+ end
+
+ --- Inspect the current async task tree.
+ ---
+ --- Returns a string representation of the task tree, showing the names and
+ --- statuses of each task.
+ --- @return string
+ function M._inspect_tree()
+ -- Inspired by https://docs.python.org/3.14/whatsnew/3.14.html#asyncio-introspection-capabilities
+ return table.concat(inspect(), '\n')
+ end
+end
return M
diff --git a/lua/gitlinker/commons/async/_util.lua b/lua/gitlinker/commons/async/_util.lua
new file mode 100644
index 00000000..b03bfa53
--- /dev/null
+++ b/lua/gitlinker/commons/async/_util.lua
@@ -0,0 +1,41 @@
+---@diagnostic disable
+local M = {}
+
+--- @param ... any
+--- @return {[integer]: any, n: integer}
+function M.pack_len(...)
+ return { n = select('#', ...), ... }
+end
+
+--- like unpack() but use the length set by F.pack_len if present
+--- @param t? { [integer]: any, n?: integer }
+--- @param first? integer
+--- @return any...
+function M.unpack_len(t, first)
+ if t then
+ return unpack(t, first or 1, t.n or table.maxn(t))
+ end
+end
+
+--- @return_cast obj function
+function M.is_callable(obj)
+ return vim.is_callable(obj)
+end
+
+--- Create a function that runs a function when it is garbage collected.
+--- @generic F : function
+--- @param f F
+--- @param gc fun()
+--- @return F
+function M.gc_fun(f, gc)
+ local proxy = newproxy(true)
+ local proxy_mt = getmetatable(proxy)
+ proxy_mt.__gc = gc
+ proxy_mt.__call = function(_, ...)
+ return f(...)
+ end
+
+ return proxy
+end
+
+return M
diff --git a/lua/gitlinker/commons/async/misc.lua b/lua/gitlinker/commons/async/misc.lua
new file mode 100644
index 00000000..de5a6405
--- /dev/null
+++ b/lua/gitlinker/commons/async/misc.lua
@@ -0,0 +1,92 @@
+---@diagnostic disable
+local async = require('gitlinker.commons.async')
+
+-- Examples of functions built on top of async.lua
+
+local M = {}
+
+--- Like async.join, but with a limit on the number of concurrent tasks.
+--- @async
+--- @param max_jobs integer
+--- @param funs (async fun())[]
+function M.join_n_1(max_jobs, funs)
+ if #funs == 0 then
+ return
+ end
+
+ max_jobs = math.min(max_jobs, #funs)
+
+ local running = {} --- @type async.Task[]
+
+ -- Start the first batch of tasks
+ for i = 1, max_jobs do
+ running[i] = assert(funs[i])()
+ end
+
+ -- As tasks finish, add new ones
+ for i = max_jobs + 1, #funs do
+ local finished = async.await_any(running)
+ --- @cast finished -?
+ running[finished] = async.run(assert(funs[i]))
+ end
+
+ -- Wait for all tasks to finish
+ async.await_all(running)
+end
+
+--- Like async.join, but with a limit on the number of concurrent tasks.
+--- (different implementation and doesn't use `async.await_any()`)
+--- @async
+--- @param max_jobs integer
+--- @param funs (async fun())[]
+function M.join_n_2(max_jobs, funs)
+ if #funs == 0 then
+ return
+ end
+
+ max_jobs = math.min(max_jobs, #funs)
+
+ --- @type (async fun())[]
+ local remaining = { select(max_jobs + 1, unpack(funs)) }
+ local to_go = #funs
+
+ async.await(1, function(finish)
+ local function cb()
+ to_go = to_go - 1
+ if to_go == 0 then
+ finish()
+ elseif #remaining > 0 then
+ local next_task = table.remove(remaining)
+ async.run(next_task):await(cb)
+ end
+ end
+
+ for i = 1, max_jobs do
+ async.run(assert(funs[i])):await(cb)
+ end
+ end)
+end
+
+--- Like async.join, but with a limit on the number of concurrent tasks.
+--- @async
+--- @param max_jobs integer
+--- @param funs (async fun())[]
+function M.join_n_3(max_jobs, funs)
+ if #funs == 0 then
+ return
+ end
+
+ local semaphore = async.semaphore(max_jobs)
+
+ local tasks = {} --- @type async.Task[]
+
+ for _, fun in ipairs(funs) do
+ tasks[#tasks + 1] = async.run(function()
+ semaphore:with(fun)
+ end)
+ end
+
+ async.await_all(tasks)
+end
+
+return M
diff --git a/lua/gitlinker/commons/fio.lua b/lua/gitlinker/commons/fio.lua
index 1d2bf72a..8118a865 100644
--- a/lua/gitlinker/commons/fio.lua
+++ b/lua/gitlinker/commons/fio.lua
@@ -2,406 +2,69 @@ local uv = vim.uv or vim.loop
local M = {}
--- FileLineReader {
-
---- @class commons.FileLineReader
---- @field filename string file name.
---- @field handler integer file handle.
---- @field filesize integer file size in bytes.
---- @field offset integer current read position.
---- @field batchsize integer chunk size for each read operation running internally.
---- @field buffer string? internal data buffer.
-local FileLineReader = {}
-
---- @param filename string
---- @param batchsize integer?
---- @return commons.FileLineReader?
-function FileLineReader:open(filename, batchsize)
- local handler = uv.fs_open(filename, "r", 438) --[[@as integer]]
- if type(handler) ~= "number" then
- error(
- string.format(
- "|commons.fio - FileLineReader:open| failed to fs_open file: %s",
- vim.inspect(filename)
- )
- )
- return nil
- end
- local fstat = uv.fs_fstat(handler) --[[@as table]]
- if type(fstat) ~= "table" then
- error(
- string.format(
- "|commons.fio - FileLineReader:open| failed to fs_fstat file: %s",
- vim.inspect(filename)
- )
- )
- uv.fs_close(handler)
- return nil
- end
-
- local o = {
- filename = filename,
- handler = handler,
- filesize = fstat.size,
- offset = 0,
- batchsize = batchsize or 4096,
- buffer = nil,
- }
- setmetatable(o, self)
- self.__index = self
- return o
-end
-
---- @private
---- @return integer
-function FileLineReader:_read_chunk()
- local chunksize = (self.filesize >= self.offset + self.batchsize) and self.batchsize
- or (self.filesize - self.offset)
- if chunksize <= 0 then
- return 0
- end
- local data, --[[@as string?]]
- read_err,
- read_name =
- uv.fs_read(self.handler, chunksize, self.offset)
- if read_err then
- error(
- string.format(
- "|commons.fio - FileLineReader:_read_chunk| failed to fs_read file: %s, read_error:%s, read_name:%s",
- vim.inspect(self.filename),
- vim.inspect(read_err),
- vim.inspect(read_name)
- )
- )
- return -1
- end
- -- append to buffer
- self.buffer = self.buffer and (self.buffer .. data) or data --[[@as string]]
- self.offset = self.offset + #data
- return #data
-end
-
---- @return boolean
-function FileLineReader:has_next()
- self:_read_chunk()
- return self.buffer ~= nil and string.len(self.buffer) > 0
-end
-
---- @return string?
-function FileLineReader:next()
- --- @return string?
- local function impl()
- local str = require("gitlinker.commons.str")
- if self.buffer == nil then
- return nil
- end
- self.buffer = self.buffer:gsub("\r\n", "\n")
- local nextpos = str.find(self.buffer, "\n")
- if nextpos then
- local line = self.buffer:sub(1, nextpos - 1)
- self.buffer = self.buffer:sub(nextpos + 1)
- return line
- else
- return nil
- end
- end
-
- repeat
- local nextline = impl()
- if nextline then
- return nextline
- end
- until self:_read_chunk() <= 0
-
- local nextline = impl()
- if nextline then
- return nextline
- else
- local buf = self.buffer
- self.buffer = nil
- return buf
- end
-end
-
--- Close the file reader.
-function FileLineReader:close()
- if self.handler then
- uv.fs_close(self.handler)
- self.handler = nil
- end
-end
-
-M.FileLineReader = FileLineReader
-
--- FileLineReader }
-
--- CachedFileReader {
-
---- @class commons.CachedFileReader
---- @field filename string
---- @field cache string?
-local CachedFileReader = {}
-
--- @param filename string
---- @return commons.CachedFileReader
-function CachedFileReader:open(filename)
- local o = {
- filename = filename,
- cache = nil,
- }
- setmetatable(o, self)
- self.__index = self
- return o
-end
-
---- @param opts {trim:boolean?}?
--- @return string?
-function CachedFileReader:read(opts)
- opts = opts or {}
- opts.trim = type(opts.trim) == "boolean" and opts.trim or false
-
- if self.cache == nil then
- self.cache = M.readfile(self.filename)
- end
- if self.cache == nil then
- return self.cache
- end
- return opts.trim and vim.trim(self.cache) or self.cache
-end
-
---- @return string?
-function CachedFileReader:reset()
- local saved = self.cache
- self.cache = nil
- return saved
-end
-
-M.CachedFileReader = CachedFileReader
-
--- CachedFileReader }
-
---- @param filename string
---- @param opts {trim:boolean?}?
---- @return string?
-M.readfile = function(filename, opts)
- opts = opts or { trim = false }
- opts.trim = type(opts.trim) == "boolean" and opts.trim or false
-
+M.readfile = function(filename)
local f = io.open(filename, "r")
if f == nil then
return nil
end
local content = f:read("*a")
f:close()
- return opts.trim and vim.trim(content) or content
+ return content
end
--- @alias commons.AsyncReadFileOnComplete fun(data:string?):any
--- @alias commons.AsyncReadFileOnError fun(step:string?,err:string?):any
--- @param filename string
---- @param opts {on_complete:commons.AsyncReadFileOnComplete,on_error:commons.AsyncReadFileOnError?,trim:boolean?}
+--- @param opts {on_complete:commons.AsyncReadFileOnComplete,on_error:commons.AsyncReadFileOnError?}
M.asyncreadfile = function(filename, opts)
assert(type(opts) == "table")
assert(type(opts.on_complete) == "function")
- opts.trim = type(opts.trim) == "boolean" and opts.trim or false
if type(opts.on_error) ~= "function" then
- opts.on_error = function(step1, err1)
+ opts.on_error = function(step, err)
error(
string.format(
"failed to read file(%s), filename:%s, error:%s",
- vim.inspect(step1),
+ vim.inspect(step),
vim.inspect(filename),
- vim.inspect(err1)
+ vim.inspect(err)
)
)
end
end
- local open_result, open_err = uv.fs_open(filename, "r", 438, function(open_complete_err, fd)
- if open_complete_err then
- opts.on_error("fs_open complete", open_complete_err)
+ uv.fs_open(filename, "r", 438, function(on_open_err, fd)
+ if on_open_err then
+ opts.on_error("fs_open", on_open_err)
return
end
- uv.fs_fstat(fd --[[@as integer]], function(fstat_complete_err, stat)
- if fstat_complete_err then
- opts.on_error("fs_fstat complete", fstat_complete_err)
+ uv.fs_fstat(fd --[[@as integer]], function(on_fstat_err, stat)
+ if on_fstat_err then
+ opts.on_error("fs_fstat", on_fstat_err)
return
end
if not stat then
- opts.on_error("fs_fstat returns nil", fstat_complete_err)
+ opts.on_error("fs_fstat", "fs_fstat returns nil")
return
end
- uv.fs_read(fd --[[@as integer]], stat.size, 0, function(read_complete_err, data)
- if read_complete_err then
- opts.on_error("fs_read complete", read_complete_err)
+ uv.fs_read(fd --[[@as integer]], stat.size, 0, function(on_read_err, data)
+ if on_read_err then
+ opts.on_error("fs_read", on_read_err)
return
end
- uv.fs_close(fd --[[@as integer]], function(close_complete_err)
- if opts.trim and type(data) == "string" then
- local trimmed_data = vim.trim(data)
- opts.on_complete(trimmed_data)
- else
- opts.on_complete(data)
- end
- if close_complete_err then
- opts.on_error("fs_close complete", close_complete_err)
+ uv.fs_close(fd --[[@as integer]], function(on_close_err)
+ opts.on_complete(data)
+ if on_close_err then
+ opts.on_error("fs_close", on_close_err)
end
end)
end)
end)
end)
- if open_result == nil then
- opts.on_error("fs_open", open_err)
- end
end
---- @param filename string
---- @return string[]|nil
-M.readlines = function(filename)
- local ok, reader = pcall(M.FileLineReader.open, M.FileLineReader, filename) --[[@as commons.FileLineReader]]
- if not ok or reader == nil then
- return nil
- end
- local results = {}
- while reader:has_next() do
- table.insert(results, reader:next())
- end
- reader:close()
- return results
-end
-
---- @alias commons.AsyncReadLinesOnLine fun(line:string):any
---- @alias commons.AsyncReadLinesOnComplete fun(bytes:integer):any
---- @alias commons.AsyncReadLinesOnError fun(step:string?,err:string?):any
---- @param filename string
---- @param opts {on_line:commons.AsyncReadLinesOnLine,on_complete:commons.AsyncReadLinesOnComplete,on_error:commons.AsyncReadLinesOnError?,batchsize:integer?}
-M.asyncreadlines = function(filename, opts)
- assert(type(opts) == "table")
- assert(type(opts.on_line) == "function")
- local batchsize = opts.batchsize or 4096
-
- if type(opts.on_error) ~= "function" then
- opts.on_error = function(step1, err1)
- error(
- string.format(
- "failed to async read file by lines(%s), filename:%s, error:%s",
- vim.inspect(step1),
- vim.inspect(filename),
- vim.inspect(err1)
- )
- )
- end
- end
-
- local open_result, open_err = uv.fs_open(filename, "r", 438, function(open_complete_err, fd)
- if open_complete_err then
- opts.on_error("fs_open complete", open_complete_err)
- return
- end
- local fstat_result, fstat_err = uv.fs_fstat(
- fd --[[@as integer]],
- function(fstat_complete_err, stat)
- if fstat_complete_err then
- opts.on_error("fs_fstat complete", fstat_complete_err)
- return
- end
- if stat == nil then
- opts.on_error("fs_fstat returns nil", fstat_complete_err)
- return
- end
-
- local fsize = stat.size
- local offset = 0
- local buffer = nil
-
- local function _process(buf, fn_line_processor)
- local str = require("gitlinker.commons.str")
-
- local i = 1
- while i <= #buf do
- local newline_pos = str.find(buf, "\n", i)
- if not newline_pos then
- break
- end
- local line = buf:sub(i, newline_pos - 1)
- fn_line_processor(line)
- i = newline_pos + 1
- end
- return i
- end
-
- local function _chunk_read()
- local read_result, read_err = uv.fs_read(
- fd --[[@as integer]],
- batchsize,
- offset,
- function(read_complete_err, data)
- if read_complete_err then
- opts.on_error("fs_read complete", read_complete_err)
- return
- end
-
- if data then
- offset = offset + #data
-
- buffer = buffer and (buffer .. data) or data --[[@as string]]
- buffer = buffer:gsub("\r\n", "\n")
- local pos = _process(buffer, opts.on_line)
- -- truncate the processed lines if still exists any
- buffer = pos <= #buffer and buffer:sub(pos, #buffer) or nil
- else
- -- no more data
-
- -- if buffer still has not been processed
- if buffer then
- local pos = _process(buffer, opts.on_line)
- buffer = pos <= #buffer and buffer:sub(pos, #buffer) or nil
-
- -- process all the left buffer till the end of file
- if buffer then
- opts.on_line(buffer)
- end
- end
-
- -- close file
- local close_result, close_err = uv.fs_close(
- fd --[[@as integer]],
- function(close_complete_err)
- if close_complete_err then
- opts.on_error("fs_close complete", close_complete_err)
- end
- if type(opts.on_complete) == "function" then
- opts.on_complete(fsize)
- end
- end
- )
- if close_result == nil then
- opts.on_error("fs_close", close_err)
- end
- end
- end
- )
- if read_result == nil then
- opts.on_error("fs_read", read_err)
- end
- end
-
- _chunk_read()
- end
- )
-
- if fstat_result == nil then
- opts.on_error("fs_fstat", fstat_err)
- end
- end)
- if open_result == nil then
- opts.on_error("fs_open", open_err)
- end
-end
-
--- AsyncFileLineReader }
-
--- @param filename string file name.
--- @param content string file content.
--- @return integer returns `0` if success, returns `-1` if failed.
@@ -415,65 +78,49 @@ M.writefile = function(filename, content)
return 0
end
+--- @alias commons.AsyncWriteFileOnComplete fun(bytes:integer?):any
+--- @alias commons.AsyncWriteFileOnError fun(step:string?,err:string?):any
--- @param filename string file name.
--- @param content string file content.
---- @param on_complete fun(bytes:integer?):any callback on write complete.
---- 1. `bytes`: written data bytes.
-M.asyncwritefile = function(filename, content, on_complete)
- uv.fs_open(filename, "w", 438, function(open_err, fd)
- if open_err then
+--- @param opts {on_complete:commons.AsyncWriteFileOnComplete,on_error:commons.AsyncWriteFileOnError?}
+M.asyncwritefile = function(filename, content, opts)
+ assert(type(opts) == "table")
+ assert(type(opts.on_complete) == "function")
+
+ if type(opts.on_error) ~= "function" then
+ opts.on_error = function(step, err)
error(
- string.format("failed to open(w) file %s: %s", vim.inspect(filename), vim.inspect(open_err))
+ string.format(
+ "failed to write file(%s), filename:%s, error:%s",
+ vim.inspect(step),
+ vim.inspect(filename),
+ vim.inspect(err)
+ )
)
+ end
+ end
+
+ uv.fs_open(filename, "w", 438, function(on_open_err, fd)
+ if on_open_err then
+ opts.on_error("fs_open", on_open_err)
return
end
---@diagnostic disable-next-line: param-type-mismatch
- uv.fs_write(fd, content, nil, function(write_err, bytes)
- if write_err then
- error(
- string.format(
- "failed to write file %s: %s",
- vim.inspect(filename),
- vim.inspect(write_err)
- )
- )
+ uv.fs_write(fd, content, nil, function(on_write_err, bytes)
+ if on_write_err then
+ opts.on_error("fs_write", on_write_err)
return
end
---@diagnostic disable-next-line: param-type-mismatch
- uv.fs_close(fd, function(close_err)
- if close_err then
- error(
- string.format(
- "failed to close(w) file %s: %s",
- vim.inspect(filename),
- vim.inspect(close_err)
- )
- )
+ uv.fs_close(fd, function(on_close_err)
+ if on_close_err then
+ opts.on_error("fs_close", on_close_err)
return
end
- if type(on_complete) == "function" then
- on_complete(bytes)
- end
+ opts.on_complete(bytes)
end)
end)
end)
end
---- @param filename string file name.
---- @param lines string[] content lines.
---- @return integer returns `0` if success, returns `-1` if failed.
-M.writelines = function(filename, lines)
- local f = io.open(filename, "w")
- if not f then
- return -1
- end
- assert(type(lines) == "table")
- for _, line in ipairs(lines) do
- assert(type(line) == "string")
- f:write(line .. "\n")
- end
- f:close()
- return 0
-end
-
return M
diff --git a/lua/gitlinker/commons/log.lua b/lua/gitlinker/commons/log.lua
new file mode 100644
index 00000000..f32a2592
--- /dev/null
+++ b/lua/gitlinker/commons/log.lua
@@ -0,0 +1,145 @@
+local uv = vim.uv or vim.loop
+local IS_WINDOWS = vim.fn.has("win32") > 0 or vim.fn.has("win64") > 0
+local PATH_SEPARATOR = IS_WINDOWS and "\\" or "/"
+
+-- :lua print(vim.inspect(vim.log.levels))
+local LogLevels = vim.log.levels
+
+local LogLevelNames = {
+ [0] = "TRACE",
+ [1] = "DEBUG",
+ [2] = "INFO",
+ [3] = "WARN",
+ [4] = "ERROR",
+ [5] = "OFF",
+}
+
+local LogHighlights = {
+ [0] = "Comment",
+ [1] = "Comment",
+ [2] = "None",
+ [3] = "WarningMsg",
+ [4] = "ErrorMsg",
+ [5] = "ErrorMsg",
+}
+
+local LogConfigs = {
+ name = nil,
+ level = LogLevels.INFO,
+ use_console = true,
+ use_file = false,
+ file_name = nil,
+}
+
+--- @param opts {name: string, level: integer?, use_console: boolean?, use_file: boolean?, file_name: string?}?
+local function setup(opts)
+ opts = opts or {}
+ local level = opts.level or LogLevels.INFO
+ local use_console
+ if type(opts.use_console) == "boolean" then
+ use_console = opts.use_console
+ else
+ use_console = true
+ end
+ local use_file = type(opts.use_file) == "boolean" and opts.use_file or false
+ local name = opts.name
+ local file_name = opts.file_name
+
+ LogConfigs.name = name
+ LogConfigs.level = level
+ LogConfigs.use_console = use_console
+ LogConfigs.use_file = use_file
+
+ -- For Windows: $env:USERPROFILE\AppData\Local\nvim-data\${file_name}.log
+ -- For *NIX: ~/.local/share/nvim/${file_name}.log
+ LogConfigs.file_name = string.format("%s%s%s", vim.fn.stdpath("data"), PATH_SEPARATOR, file_name)
+end
+
+--- @param level integer
+--- @param msg string
+local function log(level, msg)
+ if level < LogConfigs.level then
+ return
+ end
+
+ local msg_lines = vim.split(msg, "\n", { plain = true, trimempty = true })
+ if LogConfigs.use_console and level >= LogLevels.INFO then
+ local msg_chunks = {}
+ for _, line in ipairs(msg_lines) do
+ if type(line) == "string" and string.len(line) > 0 then
+ table.insert(msg_chunks, {
+ string.format("[%s] %s", LogConfigs.name, line),
+ LogHighlights[level],
+ })
+ end
+ end
+ vim.schedule(function()
+ vim.api.nvim_echo(msg_chunks, false, {})
+ end)
+ end
+ if LogConfigs.use_file then
+ local fp = io.open(LogConfigs.file_name, "a")
+ if fp then
+ for _, line in ipairs(msg_lines) do
+ local secs, ms = uv.gettimeofday()
+ fp:write(
+ string.format(
+ "%s.%03d [%s]: %s\n",
+ os.date("%Y-%m-%d %H:%M:%S", secs),
+ math.floor(ms / 1000),
+ LogLevelNames[level],
+ line
+ )
+ )
+ end
+ fp:close()
+ end
+ end
+end
+
+--- @param s string
+local function debug(s)
+ log(LogLevels.DEBUG, s)
+end
+
+--- @param s string
+local function info(s)
+ log(LogLevels.INFO, s)
+end
+
+--- @param s string
+local function warn(s)
+ log(LogLevels.WARN, s)
+end
+
+--- @param s string
+local function err(s)
+ log(LogLevels.ERROR, s)
+end
+
+--- @param s string
+local function throw(s)
+ log(LogLevels.ERROR, s)
+ error(s)
+end
+
+--- @param cond boolean
+--- @param s string
+local function ensure(cond, s)
+ if not cond then
+ throw(s)
+ end
+end
+
+local M = {
+ setup = setup,
+ debug = debug,
+ log = log,
+ info = info,
+ warn = warn,
+ err = err,
+ throw = throw,
+ ensure = ensure,
+}
+
+return M
diff --git a/lua/gitlinker/commons/logging.lua b/lua/gitlinker/commons/logging.lua
deleted file mode 100644
index 309869b8..00000000
--- a/lua/gitlinker/commons/logging.lua
+++ /dev/null
@@ -1,645 +0,0 @@
-local IS_WINDOWS = vim.fn.has("win32") > 0 or vim.fn.has("win64") > 0
-local uv = vim.uv or vim.loop
-
-local M = {}
-
--- see: `lua print(vim.inspect(vim.log.levels))`
---- @enum commons.LogLevels
-local LogLevels = {
- TRACE = 0,
- DEBUG = 1,
- INFO = 2,
- WARN = 3,
- ERROR = 4,
- OFF = 5,
-}
-
---- @enum commons.LogLevelNames
-local LogLevelNames = {
- [0] = "TRACE",
- [1] = "DEBUG",
- [2] = "INFO",
- [3] = "WARN",
- [4] = "ERROR",
- [5] = "OFF",
-}
-
-M.LogLevels = LogLevels
-M.LogLevelNames = LogLevelNames
-
-local LogHighlights = {
- [0] = "Comment",
- [1] = "Comment",
- [2] = "None",
- [3] = "WarningMsg",
- [4] = "ErrorMsg",
- [5] = "ErrorMsg",
-}
-
--- Formatter {
-
---- @class commons.logging.Formatter
---- @field fmt string
---- @field datefmt string
---- @field msecsfmt string
-local Formatter = {}
-
---- @param fmt string
---- @param opts {datefmt:string?,msecsfmt:string?}?
---- @return commons.logging.Formatter
-function Formatter:new(fmt, opts)
- assert(type(fmt) == "string")
-
- opts = opts or { datefmt = "%Y-%m-%d %H:%M:%S", msecsfmt = "%06d" }
- opts.datefmt = type(opts.datefmt) == "string" and opts.datefmt or "%Y-%m-%d %H:%M:%S"
- opts.msecsfmt = type(opts.msecsfmt) == "string" and opts.msecsfmt or "%06d"
-
- local o = {
- fmt = fmt,
- datefmt = opts.datefmt,
- msecsfmt = opts.msecsfmt,
- }
- setmetatable(o, self)
- self.__index = self
- return o
-end
-
-local FORMATTING_TAGS = {
- LEVEL_NO = "%(levelno)s",
- LEVEL_NAME = "%(levelname)s",
- MESSAGE = "%(message)s",
- ASCTIME = "%(asctime)s",
- MSECS = "%(msecs)d",
- NAME = "%(name)s",
- PROCESS = "%(process)d",
- FILE_NAME = "%(filename)s",
- LINE_NO = "%(lineno)d",
- FUNC_NAME = "%(funcName)s",
-}
-
---- @param meta table
---- @return string
-function Formatter:format(meta)
- local str = require("gitlinker.commons.str")
-
- local n = string.len(self.fmt)
-
- local function make_detect(tag)
- local function impl(idx)
- if idx - 1 >= 1 and string.sub(self.fmt, idx - 1, idx) == "%%" then
- return false
- end
-
- local endpos = idx + string.len(FORMATTING_TAGS[tag]) - 1
- if endpos > n then
- return false
- end
-
- return str.startswith(string.sub(self.fmt, idx, endpos), FORMATTING_TAGS[tag])
- end
- return impl
- end
-
- local tags = {
- "LEVEL_NO",
- "LEVEL_NAME",
- "MESSAGE",
- "ASCTIME",
- "MSECS",
- "NAME",
- "PROCESS",
- "FILE_NAME",
- "LINE_NO",
- "FUNC_NAME",
- }
-
- local builder = {}
- local i = 1
- local tmp = ""
-
- while i <= n do
- local hit = false
- for _, tag in ipairs(tags) do
- local is_tag = make_detect(tag)
- if is_tag(i) then
- if string.len(tmp) > 0 then
- table.insert(builder, tmp)
- tmp = ""
- end
- i = i + string.len(FORMATTING_TAGS[tag])
- hit = true
- if tag == "ASCTIME" then
- table.insert(builder, os.date(self.datefmt, meta.SECONDS))
- elseif tag == "MSECS" then
- table.insert(builder, string.format(self.msecsfmt, meta.MSECS))
- elseif meta[tag] ~= nil then
- table.insert(builder, tostring(meta[tag]))
- end
- break
- end
- end
-
- if not hit then
- tmp = tmp .. string.sub(self.fmt, i, i)
- i = i + 1
- end
- end
-
- return table.concat(builder, "")
-end
-
-M.Formatter = Formatter
-
--- Formatter }
-
--- Handler {
-
---- @class commons.logging.Handler
-local Handler = {}
-
---- @param meta commons.logging._MetaInfo
-function Handler:write(meta)
- assert(false)
-end
-
--- ConsoleHandler {
-
---- @class commons.logging.ConsoleHandler : commons.logging.Handler
---- @field formatter commons.logging.Formatter
-local ConsoleHandler = {}
-
---- @param formatter commons.logging.Formatter?
---- @return commons.logging.ConsoleHandler
-function ConsoleHandler:new(formatter)
- if formatter == nil then
- formatter = Formatter:new("[%(name)s] %(message)s")
- end
-
- local o = {
- formatter = formatter,
- }
- setmetatable(o, self)
- self.__index = self
- return o
-end
-
---- @param meta commons.logging._MetaInfo
-function ConsoleHandler:write(meta)
- if meta.LEVEL_NO < LogLevels.INFO then
- return
- end
-
- local msg_lines = vim.split(meta.MESSAGE, "\n", { plain = true })
- for _, line in ipairs(msg_lines) do
- local chunks = {}
- local line_meta = vim.tbl_deep_extend("force", vim.deepcopy(meta), { MESSAGE = line })
- local record = self.formatter:format(line_meta)
- table.insert(chunks, {
- record,
- LogHighlights[line_meta.LEVEL_NO],
- })
- vim.schedule(function()
- vim.api.nvim_echo(chunks, false, {})
- end)
- end
-end
-
-M.ConsoleHandler = ConsoleHandler
-
--- ConsoleHandler }
-
--- FileHandler {
-
---- @class commons.logging.FileHandler : commons.logging.Handler
---- @field formatter commons.logging.Formatter
---- @field filepath string
---- @field filemode "a"|"w"
---- @field filehandle any
-local FileHandler = {}
-
---- @param filepath string
---- @param filemode "a"|"w"|nil
---- @param formatter commons.logging.Formatter?
---- @return commons.logging.FileHandler
-function FileHandler:new(filepath, filemode, formatter)
- assert(type(filepath) == "string")
- assert(filemode == "a" or filemode == "w" or filemode == nil)
-
- if formatter == nil then
- formatter =
- Formatter:new("%(asctime)s,%(msecs)d [%(filename)s:%(lineno)d] %(levelname)s: %(message)s")
- end
-
- filemode = filemode ~= nil and string.lower(filemode) or "a"
- local filehandle = nil
-
- if filemode == "w" then
- filehandle = io.open(filepath, "w")
- assert(filehandle ~= nil, string.format("failed to open file:%s", vim.inspect(filepath)))
- end
-
- local o = {
- formatter = formatter,
- filepath = filepath,
- filemode = filemode,
- filehandle = filehandle,
- }
- setmetatable(o, self)
- self.__index = self
- return o
-end
-
-function FileHandler:close()
- if self.filemode == "w" or self.filehandle ~= nil then
- self.filehandle:close()
- self.filehandle = nil
- end
-end
-
---- @param meta commons.logging._MetaInfo
-function FileHandler:write(meta)
- local fp = nil
-
- if self.filemode == "w" then
- assert(
- self.filehandle ~= nil,
- string.format("failed to write file log:%s", vim.inspect(self.filepath))
- )
- fp = self.filehandle
- elseif self.filemode == "a" then
- fp = io.open(self.filepath, "a")
- end
-
- if fp then
- local record = self.formatter:format(meta)
- fp:write(string.format("%s\n", record))
- end
-
- if self.filemode == "a" and fp ~= nil then
- fp:close()
- end
-end
-
-M.FileHandler = FileHandler
-
--- FileHandler }
-
--- Handler }
-
--- Logger {
-
---- @class commons.logging.Logger
---- @field name string
---- @field level commons.LogLevels
---- @field handlers commons.logging.Handler[]
-local Logger = {}
-
---- @param name string
---- @param level commons.LogLevels
---- @return commons.logging.Logger
-function Logger:new(name, level)
- assert(type(name) == "string")
- assert(type(level) == "number" and LogLevelNames[level] ~= nil)
-
- local o = {
- name = name,
- level = level,
- handlers = {},
- }
- setmetatable(o, self)
- self.__index = self
- return o
-end
-
---- @param handler commons.logging.Handler
-function Logger:add_handler(handler)
- assert(type(handler) == "table")
- table.insert(self.handlers, handler)
-end
-
---- @param dbg debuginfo?
---- @param lvl integer
---- @param msg string
-function Logger:_log(dbg, lvl, msg)
- assert(type(lvl) == "number" and LogLevelNames[lvl] ~= nil)
-
- if lvl < self.level then
- return
- end
-
- for _, handler in ipairs(self.handlers) do
- local secs, millis = uv.gettimeofday()
- --- @alias commons.logging._MetaInfo {LEVEL_NO:commons.LogLevels,LEVEL_NAME:commons.LogLevelNames,MESSAGE:string,SECONDS:integer,MILLISECONDS:integer,FILE_NAME:string,LINE_NO:integer,FUNC_NAME:string}
- local meta_info = {
- LEVEL_NO = lvl,
- LEVEL_NAME = LogLevelNames[lvl],
- MESSAGE = msg,
- SECONDS = secs,
- MSECS = millis,
- NAME = self.name,
- PROCESS = uv.os_getpid(),
- FILE_NAME = dbg ~= nil and (dbg.source or dbg.short_src) or nil,
- LINE_NO = dbg ~= nil and (dbg.currentline or dbg.linedefined) or nil,
- FUNC_NAME = dbg ~= nil and (dbg.func or dbg.what) or nil,
- }
- handler:write(meta_info)
- end
-end
-
---- @param level integer|string
---- @param msg string
-function Logger:log(level, msg)
- if type(level) == "string" then
- assert(LogLevels[string.upper(level)] ~= nil)
- level = LogLevels[string.upper(level)]
- end
- assert(type(level) == "number" and LogHighlights[level] ~= nil)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- self:_log(dbg, level, msg)
-end
-
---- @param msg string
-function Logger:debug(msg)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- self:_log(dbg, LogLevels.DEBUG, msg)
-end
-
---- @param msg string
-function Logger:info(msg)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- self:_log(dbg, LogLevels.INFO, msg)
-end
-
---- @param msg string
-function Logger:warn(msg)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- self:_log(dbg, LogLevels.WARN, msg)
-end
-
---- @param msg string
-function Logger:err(msg)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- self:_log(dbg, LogLevels.ERROR, msg)
-end
-
---- @param msg string
-function Logger:throw(msg)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- self:_log(dbg, LogLevels.ERROR, msg)
- error(msg)
-end
-
---- @param cond any
---- @param msg string
-function Logger:ensure(cond, msg)
- if not cond then
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- self:_log(dbg, LogLevels.ERROR, msg)
- end
- assert(cond, msg)
-end
-
-M.Logger = Logger
-
--- Logger }
-
---- @type table
-local NAMESPACE = {}
-
---- @alias commons.LoggingConfigs {name:string,level:(commons.LogLevels|string)?,console_log:boolean?,file_log:boolean?,file_log_name:string?,file_log_dir:string?,file_log_mode:"a"|"w"|nil}
---- @type commons.LoggingConfigs
-local Defaults = {
- --- @type string
- name = nil,
- level = LogLevels.INFO,
- console_log = true,
- file_log = false,
- file_log_name = nil,
- file_log_dir = vim.fn.stdpath("data") --[[@as string]],
- file_log_mode = "a",
-}
-
---- @param opts commons.LoggingConfigs
-M.setup = function(opts)
- local conf = vim.tbl_deep_extend("force", vim.deepcopy(Defaults), opts or {})
- if type(conf.level) == "string" then
- assert(LogLevels[string.upper(conf.level)] ~= nil)
- conf.level = LogLevels[string.upper(conf.level)]
- end
- assert(type(conf.level) == "number" and LogHighlights[conf.level] ~= nil)
-
- local console_handler = ConsoleHandler:new()
- local logger = Logger:new(conf.name, conf.level --[[@as commons.LogLevels]])
- logger:add_handler(console_handler)
-
- if conf.file_log then
- assert(type(conf.file_log_name) == "string")
- local SEPARATOR = IS_WINDOWS and "\\" or "/"
- local filepath = string.format(
- "%s%s",
- type(conf.file_log_dir) == "string" and (conf.file_log_dir .. SEPARATOR) or "",
- conf.file_log_name
- )
- local file_handler = FileHandler:new(filepath, conf.file_log_mode or "a")
- logger:add_handler(file_handler)
- end
-
- M.add(logger)
-end
-
---- @param name string
---- @return boolean
-M.has = function(name)
- assert(type(name) == "string")
- return NAMESPACE[name] ~= nil
-end
-
---- @param name string
---- @return commons.logging.Logger
-M.get = function(name)
- assert(type(name) == "string")
- return NAMESPACE[name]
-end
-
---- @param logger commons.logging.Logger
-M.add = function(logger)
- assert(type(logger) == "table")
- assert((type(logger.name) == "string" and string.len(logger.name) > 0) or logger.name ~= nil)
- assert(NAMESPACE[logger.name] == nil)
- NAMESPACE[logger.name] = logger
-end
-
-local ROOT = "root"
-
---- @param level integer|string
---- @param msg string
-M.log = function(level, msg)
- if type(level) == "string" then
- assert(LogLevels[string.upper(level)] ~= nil)
- level = LogLevels[string.upper(level)]
- end
- assert(type(level) == "number" and LogHighlights[level] ~= nil)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- local logger = M.get(ROOT)
- assert(logger ~= nil)
- logger:_log(dbg, level, msg)
-end
-
---- @param msg string
-M.debug = function(msg)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- local logger = M.get(ROOT)
- assert(logger ~= nil)
- logger:_log(dbg, LogLevels.DEBUG, msg)
-end
-
---- @param msg string
-M.info = function(msg)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- local logger = M.get(ROOT)
- assert(logger ~= nil)
- logger:_log(dbg, LogLevels.INFO, msg)
-end
-
---- @param msg string
-M.warn = function(msg)
- local dbg = debug.getinfo(2, "nfSl")
- local logger = M.get(ROOT)
- assert(logger ~= nil)
- logger:_log(dbg, LogLevels.WARN, msg)
-end
-
---- @param msg string
-M.err = function(msg)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- local logger = M.get(ROOT)
- assert(logger ~= nil)
- logger:_log(dbg, LogLevels.ERROR, msg)
-end
-
---- @param msg string
-M.throw = function(msg)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- local logger = M.get(ROOT)
- assert(logger ~= nil)
- logger:_log(dbg, LogLevels.ERROR, msg)
- error(msg)
-end
-
---- @param cond any
---- @param msg string
-M.ensure = function(cond, msg)
- local dbglvl = 2
- local dbg = nil
- while true do
- dbg = debug.getinfo(dbglvl, "nfSl")
- if not dbg or dbg.what ~= "C" then
- break
- end
- dbglvl = dbglvl + 1
- end
- local logger = M.get(ROOT)
- assert(logger ~= nil)
- if not cond then
- logger:_log(dbg, LogLevels.ERROR, msg)
- end
- assert(cond, msg)
-end
-
-return M
diff --git a/lua/gitlinker/commons/num.lua b/lua/gitlinker/commons/num.lua
index 1c076873..f7d668d9 100644
--- a/lua/gitlinker/commons/num.lua
+++ b/lua/gitlinker/commons/num.lua
@@ -79,78 +79,67 @@ M.le = function(a, b, rel_tol, abs_tol)
return M.lt(a, b, rel_tol, abs_tol) or M.eq(a, b, rel_tol, abs_tol)
end
+--- @param a integer
+--- @param b integer
+--- @return integer
+M.mod = function(a, b)
+ return math.floor(math.fmod(a, b))
+end
+
--- @param value number
--- @param left number? lower bound, by default INT32_MIN
--- @param right number? upper bound, by default INT32_MAX
--- @return number
-M.bound = function(value, left, right)
+M.clamp = function(value, left, right)
assert(type(value) == "number")
assert(type(left) == "number" or left == nil)
assert(type(right) == "number" or right == nil)
return math.min(math.max(left or M.INT32_MIN, value), right or M.INT32_MAX)
end
-local IncrementalId = 0
-
---- @return integer
-M.auto_incremental_id = function()
- if IncrementalId >= M.INT32_MAX then
- IncrementalId = 1
- else
- IncrementalId = IncrementalId + 1
- end
- return IncrementalId
-end
-
---- @param a integer
---- @param b integer
---- @return integer
-M.mod = function(a, b)
- return math.floor(math.fmod(a, b))
-end
-
--- @param f fun(v:any):number
--- @param a any
--- @param ... any
--- @return integer, integer
-M.max = function(f, a, ...)
+M.min = function(f, a, ...)
assert(
type(f) == "function",
string.format("first param 'f' must be unary-function returns number value:%s", vim.inspect(f))
)
- local maximal_item = a
- local maximal_value = f(a)
- local maximal_index = 1
+ local minimal_item = a
+ local minimal_value = f(a)
+ local minimal_index = 1
for i, o in ipairs({ ... }) do
- if f(o) > maximal_value then
- maximal_item = o
- maximal_index = i
+ if f(o) < minimal_value then
+ minimal_item = o
+ minimal_index = i
end
end
- return maximal_item, maximal_index
+ return minimal_item, minimal_index
end
--- @param f fun(v:any):number
--- @param a any
--- @param ... any
--- @return integer, integer
-M.min = function(f, a, ...)
+M.max = function(f, a, ...)
assert(
type(f) == "function",
string.format("first param 'f' must be unary-function returns number value:%s", vim.inspect(f))
)
- local minimal_item = a
- local minimal_value = f(a)
- local minimal_index = 1
+ local maximal_item = a
+ local maximal_value = f(a)
+ local maximal_index = 1
for i, o in ipairs({ ... }) do
- if f(o) < minimal_value then
- minimal_item = o
- minimal_index = i
+ if f(o) > maximal_value then
+ maximal_item = o
+ maximal_index = i
end
end
- return minimal_item, minimal_index
+ return maximal_item, maximal_index
end
+-- Drop-in 32-bit random replacement of `math.random`.
--- @param m integer?
--- @param n integer?
--- @return number
@@ -180,24 +169,4 @@ M.random = function(m, n)
end
end
---- @param l any[]|string
---- @return any[]|string
-M.shuffle = function(l)
- assert(type(l) == "table")
- local n = #l
-
- local new_l = {}
- for i = 1, n do
- table.insert(new_l, l[i])
- end
-
- for i = n, 1, -1 do
- local j = M.random(n)
- local tmp = new_l[j]
- new_l[j] = new_l[i]
- new_l[i] = tmp
- end
- return new_l
-end
-
return M
diff --git a/lua/gitlinker/commons/path.lua b/lua/gitlinker/commons/path.lua
index b002b1b5..f476d041 100644
--- a/lua/gitlinker/commons/path.lua
+++ b/lua/gitlinker/commons/path.lua
@@ -59,13 +59,9 @@ M.islink = function(p)
end
--- @param p string
---- @param opts {double_backslash:boolean?}?
--- @return string
M._normalize_slash = function(p, opts)
assert(type(p) == "string")
- opts = opts or { double_backslash = false }
- opts.double_backslash = type(opts.double_backslash) == "boolean" and opts.double_backslash
- or false
-- '\\\\' => '\\'
local function _double_backslash(s)
@@ -84,9 +80,7 @@ M._normalize_slash = function(p, opts)
end
local result = vim.trim(p)
- if opts.double_backslash then
- result = _double_backslash(result)
- end
+ result = _double_backslash(result)
result = _single_backslash(result)
return result
end
@@ -121,13 +115,11 @@ M.resolve = function(p)
end
--- @param p string
---- @param opts {double_backslash:boolean?,expand:boolean?,resolve:boolean?}?
+--- @param opts {expand:boolean?,resolve:boolean?}?
--- @return string
M.normalize = function(p, opts)
assert(type(p) == "string")
- opts = opts or { double_backslash = false, expand = false, resolve = false }
- opts.double_backslash = type(opts.double_backslash) == "boolean" and opts.double_backslash
- or false
+ opts = opts or { expand = false, resolve = false }
opts.expand = type(opts.expand) == "boolean" and opts.expand or false
opts.resolve = type(opts.resolve) == "boolean" and opts.resolve or false
@@ -164,59 +156,4 @@ M.normalize = function(p, opts)
return M._normalize_slash(result, opts)
end
---- @param ... any
---- @return string
-M.join = function(...)
- return table.concat({ ... }, M.SEPARATOR)
-end
-
---- @param p string?
---- @return string
-M.reduce2home = function(p)
- return vim.fn.fnamemodify(p or vim.fn.getcwd(), ":~") --[[@as string]]
-end
-
---- @param p string?
---- @return string
-M.reduce = function(p)
- return vim.fn.fnamemodify(p or vim.fn.getcwd(), ":~:.") --[[@as string]]
-end
-
---- @param p string?
---- @return string
-M.shorten = function(p)
- return vim.fn.pathshorten(M.reduce(p)) --[[@as string]]
-end
-
---- @return string
-M.pipename = function()
- if IS_WINDOWS then
- local function uuid()
- local secs, ms = vim.loop.gettimeofday()
- return table.concat({
- string.format("%x", vim.loop.os_getpid()),
- string.format("%x", secs),
- string.format("%x", ms),
- }, "-")
- end
- return string.format([[\\.\pipe\nvim-pipe-%s]], uuid())
- else
- return vim.fn.tempname() --[[@as string]]
- end
-end
-
---- @param p string?
---- @return string?
-M.parent = function(p)
- p = p or vim.fn.getcwd()
-
- local str = require("gitlinker.commons.str")
- if str.endswith(p, "/") or str.endswith(p, "\\") then
- p = string.sub(p, 1, #p - 1)
- end
-
- local result = vim.fn.fnamemodify(p, ":h")
- return string.len(result) < string.len(p) and result or nil
-end
-
return M
diff --git a/lua/gitlinker/commons/platform.lua b/lua/gitlinker/commons/platform.lua
index 8c2ca636..a588f47d 100644
--- a/lua/gitlinker/commons/platform.lua
+++ b/lua/gitlinker/commons/platform.lua
@@ -5,7 +5,7 @@ local os_name = uv.os_uname().sysname
local os_name_valid = type(os_name) == "string" and string.len(os_name) > 0
M.OS_NAME = os_name
-M.IS_WINDOWS = os_name_valid and os_name:match("Windows") ~= nil
+M.IS_WINDOWS = vim.fn.has("win32") > 0 or vim.fn.has("win64") > 0
M.IS_MAC = os_name_valid and os_name:match("Darwin") ~= nil
M.IS_BSD = vim.fn.has("bsd") > 0
M.IS_LINUX = os_name_valid and os_name:match("Linux") ~= nil
diff --git a/lua/gitlinker/commons/spawn.lua b/lua/gitlinker/commons/spawn.lua
deleted file mode 100644
index c8516f2e..00000000
--- a/lua/gitlinker/commons/spawn.lua
+++ /dev/null
@@ -1,203 +0,0 @@
-local M = {}
-
---- @alias commons.SpawnOnLine fun(line:string):any
---- @alias commons.SpawnOnExit fun(result:{exitcode:integer?,signal:integer?}?):nil
---- @alias commons.SpawnOpts {on_stdout:commons.SpawnOnLine,on_stderr:commons.SpawnOnLine?,[string]:any}
---- @alias commons.SpawnJob {obj:vim.SystemObj,opts:commons.SpawnOpts,on_exit:commons.SpawnOnExit?}
-
---- @param cmd string[]
---- @param opts commons.SpawnOpts?
---- @param on_exit commons.SpawnOnExit?
---- @return commons.SpawnJob
-local function _impl(cmd, opts, on_exit)
- opts = opts or {}
-
- if opts.text == nil then
- opts.text = true
- end
- if type(opts.on_stderr) ~= "function" then
- opts.on_stderr = function() end
- end
-
- assert(type(opts.on_stdout) == "function", "Spawn job must have 'on_stdout' function in 'opts'")
- assert(type(opts.on_stderr) == "function", "Spawn job must have 'on_stderr' function in 'opts'")
- assert(type(on_exit) == "function" or on_exit == nil)
-
- --- @param buffer string
- --- @param fn_line_processor commons.SpawnOnLine
- --- @return integer
- local function _process(buffer, fn_line_processor)
- local str = require("gitlinker.commons.str")
-
- local i = 1
- while i <= #buffer do
- local newline_pos = str.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
-
- local stdout_buffer = nil
-
- --- @param err string?
- --- @param data string?
- local function _handle_stdout(err, data)
- if err then
- error(
- string.format(
- "failed to read stdout on cmd:%s, error:%s",
- vim.inspect(cmd),
- vim.inspect(err)
- )
- )
- return
- end
-
- if data then
- -- append data to buffer
- stdout_buffer = stdout_buffer and (stdout_buffer .. data) or data
- -- search buffer and process each line
- local i = _process(stdout_buffer, opts.on_stdout)
- -- truncate the processed lines if still exists any
- stdout_buffer = i <= #stdout_buffer and stdout_buffer:sub(i, #stdout_buffer) or nil
- elseif stdout_buffer then
- -- foreach the data_buffer and find every line
- local i = _process(stdout_buffer, opts.on_stdout)
- if i <= #stdout_buffer then
- local line = stdout_buffer:sub(i, #stdout_buffer)
- opts.on_stdout(line)
- stdout_buffer = nil
- end
- end
- end
-
- local stderr_buffer = nil
-
- --- @param err string?
- --- @param data string?
- local function _handle_stderr(err, data)
- if err then
- error(
- string.format(
- "failed to read stderr on cmd:%s, error:%s",
- vim.inspect(cmd),
- vim.inspect(err)
- )
- )
- return
- end
-
- if data then
- stderr_buffer = stderr_buffer and (stderr_buffer .. data) or data
- local i = _process(stderr_buffer, opts.on_stderr)
- stderr_buffer = i <= #stderr_buffer and stderr_buffer:sub(i, #stderr_buffer) or nil
- elseif stderr_buffer then
- local i = _process(stderr_buffer, opts.on_stderr)
- if i <= #stderr_buffer then
- local line = stderr_buffer:sub(i, #stderr_buffer)
- opts.on_stderr(line)
- stderr_buffer = nil
- end
- end
- end
-
- --- @param completed vim.SystemCompleted
- local function _handle_exit(completed)
- assert(type(on_exit) == "function")
- on_exit({ exitcode = completed.code, signal = completed.signal })
- end
-
- local obj
- if type(on_exit) == "function" then
- obj = vim.system(cmd, {
- cwd = opts.cwd,
- env = opts.env,
- clear_env = opts.clear_env,
- ---@diagnostic disable-next-line: assign-type-mismatch
- stdin = opts.stdin,
- stdout = _handle_stdout,
- stderr = _handle_stderr,
- text = opts.text,
- timeout = opts.timeout,
- detach = opts.detach,
- }, _handle_exit)
- else
- obj = vim.system(cmd, {
- cwd = opts.cwd,
- env = opts.env,
- clear_env = opts.clear_env,
- ---@diagnostic disable-next-line: assign-type-mismatch
- stdin = opts.stdin,
- stdout = _handle_stdout,
- stderr = _handle_stderr,
- text = opts.text,
- timeout = opts.timeout,
- detach = opts.detach,
- })
- end
-
- return { obj = obj, opts = opts, on_exit = on_exit }
-end
-
---- @param cmd string[]
---- @param opts commons.SpawnOpts?
---- @param on_exit commons.SpawnOnExit
---- @return commons.SpawnJob
-M.detached = function(cmd, opts, on_exit)
- opts = opts or {}
-
- assert(
- type(opts.on_stdout) == "function",
- "Detached spawn job must have 'on_stdout' function in 'opts'"
- )
- assert(opts.on_exit == nil, "Detached spawn job cannot have 'on_exit' function in 'opts'")
- assert(
- type(on_exit) == "function",
- "Detached spawn job must have 'on_exit' function in 3rd parameter"
- )
-
- return _impl(cmd, opts, on_exit)
-end
-
---- @param cmd string[]
---- @param opts commons.SpawnOpts?
---- @return commons.SpawnJob
-M.waitable = function(cmd, opts)
- opts = opts or {}
-
- assert(
- type(opts.on_stdout) == "function",
- "Waitable spawn job must have 'on_stdout' function in 'opts'"
- )
- assert(opts.on_exit == nil, "Waitable spawn job cannot have 'on_exit' function in 'opts'")
-
- return _impl(cmd, opts)
-end
-
---- @param job commons.SpawnJob
---- @param timeout integer?
---- @return {exitcode:integer?,signal:integer?}
-M.wait = function(job, timeout)
- assert(type(job) == "table", "Spawn job must be a 'commons.SpawnJob' object")
- assert(job.obj ~= nil, "Spawn job must be a 'commons.SpawnJob' object")
- assert(type(job.opts) == "table", "Spawn job must be a 'commons.SpawnJob' object")
- assert(
- job.on_exit == nil,
- "Detached spawn job cannot 'wait' for its exit, it already has 'on_exit' in 3rd parameter for its exit"
- )
-
- local completed
- if type(timeout) == "number" and timeout >= 0 then
- completed = job.obj:wait(timeout) --[[@as vim.SystemCompleted]]
- else
- completed = job.obj:wait() --[[@as vim.SystemCompleted]]
- end
- return { exitcode = completed.code, signal = completed.signal }
-end
-
-return M
diff --git a/lua/gitlinker/commons/str.lua b/lua/gitlinker/commons/str.lua
index 0512ae9d..26c8fe94 100644
--- a/lua/gitlinker/commons/str.lua
+++ b/lua/gitlinker/commons/str.lua
@@ -269,7 +269,6 @@ M.setchar = function(s, pos, ch)
assert(string_len(ch) == 1)
local n = string_len(s)
- pos = require("gitlinker.commons.tbl").list_index(pos, n)
local buffer = ""
if pos > 1 then
diff --git a/lua/gitlinker/commons/tbl.lua b/lua/gitlinker/commons/tbl.lua
index c91ef17a..82acef5b 100644
--- a/lua/gitlinker/commons/tbl.lua
+++ b/lua/gitlinker/commons/tbl.lua
@@ -12,25 +12,6 @@ M.tbl_not_empty = function(t)
return type(t) == "table" and not vim.tbl_isempty(t)
end
---- @param t any?
---- @param ... any
---- @return any
-M.tbl_get = function(t, ...)
- local args = { ... }
- if #args == 0 then
- return t
- end
- local e = t --[[@as table]]
- for _, k in ipairs(args) do
- if type(e) == "table" and e[k] ~= nil then
- e = e[k]
- else
- return nil
- end
- end
- return e
-end
-
--- @param t any[]
--- @param v any
--- @param compare (fun(a:any, b:any):boolean)|nil
@@ -63,14 +44,6 @@ M.list_not_empty = function(l)
return type(l) == "table" and #l > 0
end
---- @param i integer
---- @param n integer
---- @return integer
-M.list_index = function(i, n)
- assert(n >= 0)
- return i > 0 and i or (n + i + 1)
-end
-
--- @param l any[]
--- @param v any
--- @param compare (fun(a:any, b:any):boolean)|nil
@@ -91,644 +64,4 @@ M.list_contains = function(l, v, compare)
return false
end
---- @class commons.List
---- @field _data any[]
-local List = {}
-
---- @param l any[]
---- @return commons.List
-function List:move(l)
- assert(type(l) == "table")
-
- local o = { _data = l }
- setmetatable(o, self)
- self.__index = self
- return o
-end
-
---- @param l any[]
---- @return commons.List
-function List:copy(l)
- assert(type(l) == "table")
-
- local new_l = {}
- for i, v in ipairs(l) do
- table.insert(new_l, v)
- end
- return List:move(new_l)
-end
-
---- @param ... any
---- @return commons.List
-function List:of(...)
- return List:move({ ... })
-end
-
---- @return any[]
-function List:data()
- return self._data
-end
-
---- @return integer
-function List:length()
- return #self._data
-end
-
---- @return boolean
-function List:empty()
- return #self._data == 0
-end
-
---- @param index integer
---- @return any
-function List:at(index)
- local normalized_index = M.list_index(index, self:length())
- return self._data[normalized_index]
-end
-
---- @return any
-function List:first()
- return self:at(1)
-end
-
---- @return any
-function List:last()
- return self:at(self:length())
-end
-
---- @param other commons.List
---- @return commons.List
-function List:concat(other)
- assert(M.is_list(other))
- local l = {}
- for i, v in ipairs(self._data) do
- table.insert(l, v)
- end
- for i, v in ipairs(other._data) do
- table.insert(l, v)
- end
- return List:move(l)
-end
-
---- @param separator string?
---- @return string
-function List:join(separator)
- separator = separator or " "
- return table.concat(self._data, separator)
-end
-
---- @param f fun(value:any, index:integer):boolean
---- @return boolean
-function List:every(f)
- assert(type(f) == "function")
- for i, v in ipairs(self._data) do
- if not f(v, i) then
- return false
- end
- end
- return true
-end
-
---- @param f fun(value:any, index:integer):boolean
---- @return boolean
-function List:some(f)
- assert(type(f) == "function")
- for i, v in ipairs(self._data) do
- if f(v, i) then
- return true
- end
- end
- return false
-end
-
---- @param f fun(value:any, index:integer):boolean
---- @return boolean
-function List:none(f)
- assert(type(f) == "function")
- for i, v in ipairs(self._data) do
- if f(v, i) then
- return false
- end
- end
- return true
-end
-
---- @param f fun(value:any, index:integer):boolean
---- @return commons.List
-function List:filter(f)
- assert(type(f) == "function")
- local l = {}
- for i, v in ipairs(self._data) do
- if f(v, i) then
- table.insert(l, v)
- end
- end
- return List:move(l)
-end
-
---- @param f fun(value:any, index:integer):boolean
---- @return any?, integer
-function List:find(f)
- assert(type(f) == "function")
- for i, v in ipairs(self._data) do
- if f(v, i) then
- return v, i
- end
- end
- return nil, -1
-end
-
---- @param f fun(value:any, index:integer):boolean
---- @return any?, integer
-function List:findLast(f)
- assert(type(f) == "function")
- local n = self:length()
-
- for i = n, 1, -1 do
- local v = self._data[i]
- if f(v, i) then
- return v, i
- end
- end
- return nil, -1
-end
-
---- @param value any
---- @param start integer?
---- @param comparator (fun(a:any,b:any):boolean)|nil
---- @return integer?
-function List:indexOf(value, start, comparator)
- assert(type(comparator) == "function" or comparator == nil)
- start = start or 1
- local n = self:length()
-
- for i = start, n do
- local v = self._data[i]
- if type(comparator) == "function" then
- if comparator(v, value) then
- return i
- end
- else
- if v == value then
- return i
- end
- end
- end
-
- return -1
-end
-
---- @param value any
---- @param rstart integer?
---- @param comparator (fun(a:any,b:any):boolean)|nil
---- @return integer?
-function List:lastIndexOf(value, rstart, comparator)
- assert(type(comparator) == "function" or comparator == nil)
- local n = self:length()
- rstart = rstart or n
-
- for i = rstart, 1, -1 do
- local v = self._data[i]
- if type(comparator) == "function" then
- if comparator(v, value) then
- return i
- end
- else
- if v == value then
- return i
- end
- end
- end
-
- return -1
-end
-
---- @param f fun(value:any, index:integer):nil
-function List:forEach(f)
- assert(type(f) == "function")
- for i, v in ipairs(self._data) do
- f(v, i)
- end
-end
-
---- @param value any
---- @param start integer?
---- @param comparator (fun(a:any,b:any):boolean)|nil
---- @return boolean
-function List:includes(value, start, comparator)
- return self:indexOf(value, start, comparator) >= 1
-end
-
---- @param f fun(value:any,index:integer):any
---- @return commons.List
-function List:map(f)
- assert(type(f) == "function")
- local l = {}
- for i, v in ipairs(self._data) do
- table.insert(l, f(v, i))
- end
- return List:move(l)
-end
-
---- @return any?, boolean
-function List:pop()
- if self:empty() then
- return nil, false
- end
- return table.remove(self._data, self:length()), true
-end
-
---- @param ... any
-function List:push(...)
- for i, v in ipairs({ ... }) do
- table.insert(self._data, v)
- end
-end
-
---- @return any?, boolean
-function List:shift()
- if self:empty() then
- return nil, false
- end
- return table.remove(self._data, 1), true
-end
-
---- @param ... any
-function List:unshift(...)
- for i, v in ipairs({ ... }) do
- table.insert(self._data, 1, v)
- end
-end
-
---- @param f fun(accumulator:any,value:any,index:integer):any
---- @param initialValue any?
---- @return any
-function List:reduce(f, initialValue)
- assert(type(f) == "function")
-
- if self:empty() then
- return initialValue
- end
-
- local startIndex = initialValue and 1 or 2
- local accumulator = initialValue or self._data[1]
- local n = self:length()
- local i = startIndex
- while i <= n do
- accumulator = f(accumulator, self._data[i], i)
- i = i + 1
- end
- return accumulator
-end
-
---- @param f fun(accumulator:any,value:any,index:integer):any
---- @param initialValue any?
---- @return any
-function List:reduceRight(f, initialValue)
- assert(type(f) == "function")
-
- if self:empty() then
- return initialValue
- end
-
- local n = self:length()
- local startIndex = initialValue and n or self:length() - 1
- local accumulator = initialValue or self._data[n]
-
- local i = startIndex
- while i >= 1 do
- accumulator = f(accumulator, self._data[i], i)
- i = i - 1
- end
- return accumulator
-end
-
---- @return commons.List
-function List:reverse()
- if self:empty() then
- return List:move({})
- end
-
- local l = {}
- local i = self:length()
- while i >= 1 do
- table.insert(l, self._data[i])
- i = i - 1
- end
- return List:move(l)
-end
-
---- @param startIndex integer?
---- @param endIndex integer?
---- @return commons.List
-function List:slice(startIndex, endIndex)
- assert(type(startIndex) == "number" or startIndex == nil)
- assert(type(endIndex) == "number" or endIndex == nil)
-
- local n = self:length()
- startIndex = startIndex or 1
- endIndex = endIndex or n
-
- local l = {}
- for i = startIndex, endIndex do
- if i >= 1 and i <= n then
- table.insert(l, self._data[i])
- end
- end
- return List:move(l)
-end
-
---- @param comparator (fun(a:any,b:any):boolean)|nil
---- @return commons.List
-function List:sort(comparator)
- local l = {}
- for i, v in ipairs(self._data) do
- table.insert(l, v)
- end
- table.sort(l, comparator)
- return List:move(l)
-end
-
-M.List = List
-
---- @param o any
---- @return boolean
-M.is_list = function(o)
- return type(o) == "table" and o.__index == List and getmetatable(o) == List
-end
-
---- @class commons.HashMap
---- @field _data table
-local HashMap = {}
-
---- @param t table
---- @return commons.HashMap
-function HashMap:move(t)
- assert(type(t) == "table")
-
- local o = { _data = t }
- setmetatable(o, self)
- self.__index = self
- return o
-end
-
---- @param t table
---- @return commons.HashMap
-function HashMap:copy(t)
- assert(type(t) == "table")
-
- local new_t = {}
- for k, v in pairs(t) do
- new_t[k] = v
- end
- return HashMap:move(new_t)
-end
-
---- @param ... {[1]:any,[2]:any}
---- @return commons.HashMap
-function HashMap:of(...)
- local t = {}
- local s = 0
- for i, v in ipairs({ ... }) do
- t[v[1]] = v[2]
- s = s + 1
- end
- local o = { _data = t }
- setmetatable(o, self)
- self.__index = self
- return o
-end
-
---- @return table
-function HashMap:data()
- return self._data
-end
-
---- @return integer
-function HashMap:size()
- local s = 0
- for _, _ in pairs(self._data) do
- s = s + 1
- end
- return s
-end
-
---- @return boolean
-function HashMap:empty()
- return next(self._data) == nil
-end
-
---- @param key any
---- @param value any
-function HashMap:set(key, value)
- self._data[key] = value
-end
-
---- @param key any
---- @return any?
-function HashMap:unset(key)
- local old = self._data[key]
- self._data[key] = nil
- return old
-end
-
---- @param ... any
---- @return any
-function HashMap:get(...)
- return M.tbl_get(self._data, ...)
-end
-
---- @param key any
---- @return boolean
-function HashMap:hasKey(key)
- return self._data[key] ~= nil
-end
-
---- @param value any
---- @param comparator (fun(a:any, b:any):boolean)|nil
---- @return boolean
-function HashMap:hasValue(value, comparator)
- for k, v in pairs(self._data) do
- if type(comparator) == "function" and comparator(v, value) then
- return true
- elseif v == value then
- return true
- end
- end
- return false
-end
-
---- @param other commons.HashMap
---- @return commons.HashMap
-function HashMap:merge(other)
- assert(M.is_hashmap(other))
- local t = {}
- for k, v in pairs(self._data) do
- t[k] = v
- end
- for k, v in pairs(other._data) do
- t[k] = v
- end
- return HashMap:move(t)
-end
-
---- @param f fun(key:any, value:any):boolean
---- @return boolean
-function HashMap:every(f)
- assert(type(f) == "function")
- for k, v in pairs(self._data) do
- if not f(k, v) then
- return false
- end
- end
- return true
-end
-
---- @param f fun(key:any, value:any):boolean
---- @return boolean
-function HashMap:some(f)
- assert(type(f) == "function")
- for k, v in pairs(self._data) do
- if f(k, v) then
- return true
- end
- end
- return false
-end
-
---- @param f fun(key:any, value:any):boolean
---- @return boolean
-function HashMap:none(f)
- assert(type(f) == "function")
- for k, v in pairs(self._data) do
- if f(k, v) then
- return false
- end
- end
- return true
-end
-
---- @param f fun(key:any, value:any):boolean
---- @return commons.HashMap
-function HashMap:filter(f)
- assert(type(f) == "function")
- local t = {}
- for k, v in pairs(self._data) do
- if f(k, v) then
- t[k] = v
- end
- end
- return HashMap:move(t)
-end
-
---- @param f fun(key:any, value:any):boolean
---- @return any, any
-function HashMap:find(f)
- assert(type(f) == "function")
- for k, v in pairs(self._data) do
- if f(k, v) then
- return k, v
- end
- end
- return nil, nil
-end
-
---- @param f fun(key:any,value:any):nil
-function HashMap:forEach(f)
- assert(type(f) == "function")
-
- for k, v in pairs(self._data) do
- f(k, v)
- end
-end
-
---- @param iterator any?
---- @return any, any
-function HashMap:next(iterator)
- return next(self._data, iterator)
-end
-
---- @return commons.HashMap
-function HashMap:invert()
- local t = {}
- for k, v in pairs(self._data) do
- t[v] = k
- end
- return HashMap:move(t)
-end
-
---- @param f fun(key:any, value:any):any
---- @return commons.HashMap
-function HashMap:mapKeys(f)
- assert(type(f) == "function")
- local t = {}
- for k, v in pairs(self._data) do
- t[f(k, v)] = v
- end
- return HashMap:move(t)
-end
-
---- @param f fun(key:any, value:any):any
---- @return commons.HashMap
-function HashMap:mapValues(f)
- assert(type(f) == "function")
- local t = {}
- for k, v in pairs(self._data) do
- t[k] = f(k, v)
- end
- return HashMap:move(t)
-end
-
---- @return any[]
-function HashMap:keys()
- local keys = {}
- for k, _ in pairs(self._data) do
- table.insert(keys, k)
- end
- return keys
-end
-
---- @return any[]
-function HashMap:values()
- local values = {}
- for _, v in pairs(self._data) do
- table.insert(values, v)
- end
- return values
-end
-
---- @return {[1]:any,[2]:any}[]
-function HashMap:entries()
- local p = {}
- for k, v in pairs(self._data) do
- table.insert(p, { k, v })
- end
- return p
-end
-
---- @param f fun(accumulator:any,key:any,value:any):any
---- @param initialValue any
---- @return any
-function HashMap:reduce(f, initialValue)
- assert(type(f) == "function")
-
- if self:empty() then
- return initialValue
- end
-
- local accumulator = initialValue
- for k, v in pairs(self._data) do
- accumulator = f(accumulator, k, v)
- end
- return accumulator
-end
-
-M.HashMap = HashMap
-
---- @param o any?
---- @return boolean
-M.is_hashmap = function(o)
- return type(o) == "table" and o.__index == HashMap and getmetatable(o) == HashMap
-end
-
return M
diff --git a/lua/gitlinker/commons/uv.lua b/lua/gitlinker/commons/uv.lua
deleted file mode 100644
index 8a70d613..00000000
--- a/lua/gitlinker/commons/uv.lua
+++ /dev/null
@@ -1 +0,0 @@
-return vim.uv or vim.loop
diff --git a/lua/gitlinker/commons/version.lua b/lua/gitlinker/commons/version.lua
deleted file mode 100644
index e44c0490..00000000
--- a/lua/gitlinker/commons/version.lua
+++ /dev/null
@@ -1,50 +0,0 @@
-local M = {}
-
-M.HAS_VIM_VERSION_EQ = type(vim.version) == "table" and vim.is_callable(vim.version.eq)
-M.HAS_VIM_VERSION_GT = type(vim.version) == "table" and vim.is_callable(vim.version.gt)
-M.HAS_VIM_VERSION_GE = type(vim.version) == "table" and vim.is_callable(vim.version.ge)
-M.HAS_VIM_VERSION_LT = type(vim.version) == "table" and vim.is_callable(vim.version.lt)
-M.HAS_VIM_VERSION_LE = type(vim.version) == "table" and vim.is_callable(vim.version.le)
-
---- @param l integer[]
---- @return string
-M.to_string = function(l)
- assert(type(l) == "table")
- local builder = {}
- for _, v in ipairs(l) do
- table.insert(builder, tostring(v))
- end
- return table.concat(builder, ".")
-end
-
---- @param s string
---- @return integer[]
-M.to_list = function(s)
- assert(type(s) == "string")
- local splits = vim.split(s, ".", { plain = true })
- local result = {}
- for _, v in ipairs(splits) do
- table.insert(result, tonumber(v))
- end
- return result
-end
-
---- @param ver string|integer[]
---- @return boolean
-M.lt = function(ver)
- if type(ver) == "string" then
- ver = M.to_list(ver)
- end
- return vim.version.lt(vim.version(), ver)
-end
-
---- @param ver string|integer[]
---- @return boolean
-M.ge = function(ver)
- if type(ver) == "string" then
- ver = M.to_list(ver)
- end
- return vim.version.gt(vim.version(), ver) or vim.version.eq(vim.version(), ver)
-end
-
-return M
diff --git a/lua/gitlinker/commons/version.txt b/lua/gitlinker/commons/version.txt
deleted file mode 100644
index 008c39a4..00000000
--- a/lua/gitlinker/commons/version.txt
+++ /dev/null
@@ -1 +0,0 @@
-27.0.0
diff --git a/lua/gitlinker/git.lua b/lua/gitlinker/git.lua
index 6a288744..41fa05f8 100644
--- a/lua/gitlinker/git.lua
+++ b/lua/gitlinker/git.lua
@@ -1,9 +1,7 @@
-local logging = require("gitlinker.commons.logging")
-local spawn = require("gitlinker.commons.spawn")
-local uv = require("gitlinker.commons.uv")
+local log = require("gitlinker.commons.log")
local str = require("gitlinker.commons.str")
-
-local async = require("gitlinker.async")
+local async = require("gitlinker.commons.async")
+local uv = vim.uv or vim.loop
--- @class gitlinker.CmdResult
--- @field stdout string[]
@@ -33,44 +31,46 @@ end
--- @param default string
function CmdResult:print_err(default)
- local logger = logging.get("gitlinker")
if self:has_err() then
for _, e in ipairs(self.stderr) do
- logger:err(e)
+ log.err(e)
end
else
- logger:err("fatal: " .. default)
+ log.err("fatal: " .. default)
end
end
---- NOTE: async functions can't have optional parameters so wrap it into another function without '_'
-local _run_cmd = async.wrap(function(args, cwd, callback)
- local result = CmdResult:new()
- local logger = logging.get("gitlinker")
- logger:debug(string.format("|_run_cmd| args:%s, cwd:%s", vim.inspect(args), vim.inspect(cwd)))
+--- @param args string[]
+--- @param cwd string?
+--- @param callback fun(gitlinker.CmdResult):any
+local function _run_cmd_async(args, cwd, callback)
+ log.debug(string.format("|_run_cmd_async| args:%s, cwd:%s", vim.inspect(args), vim.inspect(cwd)))
+
+ --- @param completed vim.SystemCompleted
+ local function on_exit(completed)
+ local result = CmdResult:new()
+ if str.not_blank(completed.stdout) then
+ result.stdout = str.split(str.trim(completed.stdout), "\n", { trimempty = true })
+ end
+ if str.not_blank(completed.stderr) then
+ result.stderr = str.split(str.trim(completed.stderr), "\n", { trimempty = true })
+ end
+ log.debug(string.format("|_run_cmd_async| result:%s", vim.inspect(result)))
+ callback(result)
+ end
- spawn.detached(args, {
+ vim.system(args, {
cwd = cwd,
- on_stdout = function(line)
- if type(line) == "string" then
- table.insert(result.stdout, line)
- end
- end,
- on_stderr = function(line)
- if type(line) == "string" then
- table.insert(result.stderr, line)
- end
- end,
- }, function()
- logger:debug(string.format("|_run_cmd| result:%s", vim.inspect(result)))
- callback(result)
- end)
-end, 3)
+ text = true,
+ }, on_exit)
+end
-- wrap the git command to do the right thing always
--- @package
--- @type fun(args:string[], cwd:string?): gitlinker.CmdResult
local function run_cmd(args, cwd)
+ --- @type fun(args:string[], cwd:string?): gitlinker.CmdResult
+ local _run_cmd = async.wrap(3, _run_cmd_async)
return _run_cmd(args, cwd or uv.cwd())
end
@@ -274,7 +274,6 @@ end
--- @param cwd string?
--- @return string?
local function get_closest_remote_compatible_rev(remote, cwd)
- local logger = logging.get("gitlinker")
assert(remote, "remote cannot be nil")
-- try upstream branch HEAD (a.k.a @{u})
@@ -331,7 +330,7 @@ local function get_closest_remote_compatible_rev(remote, cwd)
return remote_rev
end
- logger:err("fatal: failed to get closest revision in that exists in remote: " .. remote)
+ log.err("fatal: failed to get closest revision in that exists in remote: " .. remote)
return nil
end
@@ -383,7 +382,6 @@ end
--- @param cwd string?
--- @return string?
local function _select_remotes(remotes, cwd)
- local logger = logging.get("gitlinker")
-- local result = run_select(remotes)
local formatted_remotes = { "Please select remote index:" }
@@ -392,12 +390,13 @@ local function _select_remotes(remotes, cwd)
table.insert(formatted_remotes, string.format("%d. %s (%s)", i, remote, remote_url))
end
- async.scheduler()
+ async.await(1, vim.schedule)
+
local result = vim.fn.inputlist(formatted_remotes)
-- logger:debug(string.format("inputlist:%s(%s)", vim.inspect(result), vim.inspect(type(result))))
if type(result) ~= "number" or result < 1 or result > #remotes then
- logger:err("fatal: user cancelled multiple git remotes")
+ log.err("fatal: user cancelled multiple git remotes")
return nil
end
@@ -407,17 +406,16 @@ local function _select_remotes(remotes, cwd)
end
end
- logger:err("fatal: user cancelled multiple git remotes, please select an index")
+ log.err("fatal: user cancelled multiple git remotes, please select an index")
return nil
end
--- @param cwd string?
--- @return string?
local function get_branch_remote(cwd)
- local logger = logging.get("gitlinker")
-- origin/upstream
local remotes = _get_remote(cwd)
- logger:debug(string.format("git remotes:%s", vim.inspect(remotes)))
+ log.debug(string.format("git remotes:%s", vim.inspect(remotes)))
if not remotes then
return nil
end
@@ -443,7 +441,7 @@ local function get_branch_remote(cwd)
upstream_branch:match("^(" .. upstream_branch_allowed_chars .. ")%/")
if not remote_from_upstream_branch then
- logger:err("fatal: cannot parse remote name from remote branch: " .. upstream_branch)
+ log.err("fatal: cannot parse remote name from remote branch: " .. upstream_branch)
return nil
end
@@ -453,7 +451,7 @@ local function get_branch_remote(cwd)
end
end
- logger:err(
+ log.err(
string.format(
"fatal: parsed remote '%s' from remote branch '%s' is not a valid remote",
remote_from_upstream_branch,
@@ -467,13 +465,12 @@ end
--- @param cwd string?
--- @return string?
local function get_default_branch(remote, cwd)
- local logger = logging.get("gitlinker")
local args = { "git", "rev-parse", "--abbrev-ref", string.format("%s/HEAD", remote) }
local result = run_cmd(args, cwd)
if type(result.stdout) ~= "table" or #result.stdout == 0 then
return nil
end
- logger:debug(
+ log.debug(
string.format(
"|get_default_branch| running %s: %s",
vim.inspect(args),
@@ -487,13 +484,12 @@ end
--- @param cwd string?
--- @return string?
local function get_current_branch(cwd)
- local logger = logging.get("gitlinker")
local args = { "git", "rev-parse", "--abbrev-ref", "HEAD" }
local result = run_cmd(args, cwd)
if type(result.stdout) ~= "table" or #result.stdout == 0 then
return nil
end
- logger:debug(
+ log.debug(
string.format(
"|get_current_branch| running %s: %s",
vim.inspect(args),
diff --git a/lua/gitlinker/linker.lua b/lua/gitlinker/linker.lua
index e5a4b81a..656eb0f7 100644
--- a/lua/gitlinker/linker.lua
+++ b/lua/gitlinker/linker.lua
@@ -1,17 +1,16 @@
-local logging = require("gitlinker.commons.logging")
+local log = require("gitlinker.commons.log")
local str = require("gitlinker.commons.str")
+local async = require("gitlinker.commons.async")
-local async = require("gitlinker.async")
local git = require("gitlinker.git")
local path = require("gitlinker.path")
local giturlparser = require("gitlinker.giturlparser")
--- @return string?
local function _get_buf_dir()
- local logger = logging.get("gitlinker")
local buf_path = vim.api.nvim_buf_get_name(0)
local buf_dir = vim.fn.fnamemodify(buf_path, ":p:h")
- logger:debug(
+ log.debug(
string.format(
"|_get_buf_dir| buf_path:%s, buf_dir:%s",
vim.inspect(buf_path),
@@ -30,7 +29,6 @@ end
--- @param rev string?
--- @return gitlinker.Linker?
local function make_linker(remote, file, rev)
- local logger = logging.get("gitlinker")
local cwd = _get_buf_dir()
local file_provided = str.not_empty(file)
@@ -55,7 +53,7 @@ local function make_linker(remote, file, rev)
end
local parsed_url, parsed_err = giturlparser.parse(remote_url) --[[@as table, string?]]
- logger:debug(
+ log.debug(
string.format(
"|make_linker| remote:%s, parsed_url:%s, parsed_err:%s",
vim.inspect(remote),
@@ -63,7 +61,7 @@ local function make_linker(remote, file, rev)
vim.inspect(parsed_err)
)
)
- logger:ensure(
+ log.ensure(
parsed_url ~= nil,
string.format(
"failed to parse git remote url:%s, error:%s",
@@ -89,7 +87,7 @@ local function make_linker(remote, file, rev)
end
-- logger.debug("|linker - Linker:make| rev:%s", vim.inspect(rev))
- async.scheduler()
+ async.await(1, vim.schedule)
if not file_provided then
local buf_path_on_root = path.buffer_relpath(root) --[[@as string]]
@@ -114,7 +112,7 @@ local function make_linker(remote, file, rev)
-- vim.inspect(file_in_rev_result)
-- )
- async.scheduler()
+ async.await(1, vim.schedule)
local file_changed = false
if not file_provided then
@@ -151,7 +149,7 @@ local function make_linker(remote, file, rev)
current_branch = current_branch,
}
- logger:debug(string.format("|make_linker| o:%s", vim.inspect(o)))
+ log.debug(string.format("|make_linker| o:%s", vim.inspect(o)))
return o
end
diff --git a/lua/gitlinker/range.lua b/lua/gitlinker/range.lua
index 26d9a8be..1355df24 100644
--- a/lua/gitlinker/range.lua
+++ b/lua/gitlinker/range.lua
@@ -1,4 +1,4 @@
-local logging = require("gitlinker.commons.logging")
+local log = require("gitlinker.commons.log")
--- @param m string
--- @return boolean
@@ -13,7 +13,7 @@ end
--- @return gitlinker.Range
local function make_range()
local m = vim.fn.mode()
- logging.get("gitlinker"):debug(string.format("|make_range| mode:%s", vim.inspect(m)))
+ log.debug(string.format("|make_range| mode:%s", vim.inspect(m)))
local l1 = nil
local l2 = nil
if _is_visual_mode(m) then
diff --git a/lua/gitlinker/routers.lua b/lua/gitlinker/routers.lua
index eacea782..f3061024 100644
--- a/lua/gitlinker/routers.lua
+++ b/lua/gitlinker/routers.lua
@@ -1,5 +1,5 @@
local str = require("gitlinker.commons.str")
-local logging = require("gitlinker.commons.logging")
+local log = require("gitlinker.commons.log")
local range = require("gitlinker.range")
@@ -114,9 +114,7 @@ end
--- @param lk gitlinker.Linker
--- @return string
local function samba_browse(lk)
- local logger = logging.get("gitlinker")
-
- logger:debug(string.format("|samba_browse| lk:%s", vim.inspect(lk)))
+ log.debug(string.format("|samba_browse| lk:%s", vim.inspect(lk)))
local builder = "https://git.samba.org/?p="
-- org
builder = builder .. (string.len(lk.org) > 0 and string.format("%s/", lk.org) or "")
diff --git a/spec/gitlinker/gitlinker/git_spec.lua b/spec/gitlinker/gitlinker/git_spec.lua
index e3da6ba1..6801ef09 100644
--- a/spec/gitlinker/gitlinker/git_spec.lua
+++ b/spec/gitlinker/gitlinker/git_spec.lua
@@ -11,7 +11,7 @@ describe("gitlinker.git", function()
vim.cmd([[ edit lua/gitlinker.lua ]])
end)
- local async = require("gitlinker.async")
+ local async = require("gitlinker.commons.async")
local git = require("gitlinker.git")
local path = require("gitlinker.path")
local gitlinker = require("gitlinker")
diff --git a/spec/gitlinker/gitlinker/linker_spec.lua b/spec/gitlinker/gitlinker/linker_spec.lua
index 240243eb..e3a3cd61 100644
--- a/spec/gitlinker/gitlinker/linker_spec.lua
+++ b/spec/gitlinker/gitlinker/linker_spec.lua
@@ -13,7 +13,7 @@ describe("gitlinker.linker", function()
vim.cmd([[ edit lua/gitlinker.lua ]])
end)
- local async = require("gitlinker.async")
+ local async = require("gitlinker.commons.async")
local github_actions = os.getenv("GITHUB_ACTIONS") == "true"
local linker = require("gitlinker.linker")
describe("[make_linker]", function()