diff --git a/.github/workflows/bump-commons.yml b/.github/workflows/bump-commons.yml
index 960f2c2a..c99a3c01 100644
--- a/.github/workflows/bump-commons.yml
+++ b/.github/workflows/bump-commons.yml
@@ -6,7 +6,7 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
bump:
- name: Lint
+ name: Bump
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -19,9 +19,10 @@ jobs:
git clone --depth=1 https://github.com/linrongbin16/commons.nvim.git ~/.commons.nvim
rm -rf ./lua/colorbox/commons
mkdir -p ./lua/colorbox/commons
- cp -rf ~/.commons.nvim/lua/commons/*.lua ./lua/colorbox/commons
+ cp -rf ~/.commons.nvim/lua/commons ./lua/colorbox
cd ./lua/colorbox/commons
find . -type f -name '*.lua' -exec sed -i 's/require("commons/require("colorbox.commons/g' {} \;
+ find . -type f -name '*.lua' -exec sed -i "s/require('commons/require('colorbox.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 2d1135ed..30862365 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,47 +38,20 @@ jobs:
run: |
cargo binstall --no-confirm selene
selene --config selene.toml ./lua
- # unit_test:
- # name: Unit Test
- # strategy:
- # matrix:
- # nvim_version: [stable, nightly]
- # runs-on: ubuntu-latest
- # steps:
- # - uses: actions/checkout@v4
- # - uses: rhysd/action-setup-vim@v1
- # id: vim
- # with:
- # neovim: true
- # version: ${{ matrix.nvim_version }}
- # - uses: leafo/gh-actions-lua@v10
- # with:
- # # luaVersion: "luajit-2.1.0-beta3"
- # luaVersion: "luajit-openresty"
- # - uses: leafo/gh-actions-luarocks@v4
- # - name: Run Tests
- # shell: bash
- # run: |
- # luarocks install luacov
- # luarocks install luacov-reporter-lcov
- # luarocks --lua-version=5.1 install vusted
- # vusted --coverage ./spec
- # echo "ls -l ."
- # ls -l .
- # - name: Generate Coverage Report
- # shell: bash
- # run: |
- # echo "luacov -r lcov"
- # luacov -r lcov
- # echo "ls -l ."
- # ls -l .
- # echo "tail ./luacov.report.out"
- # tail -n 10 ./luacov.report.out
- # - uses: codecov/codecov-action@v4
- # with:
- # files: luacov.report.out
- # env:
- # CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ unit_test:
+ name: Unit Test
+ strategy:
+ matrix:
+ nvim_version: [stable, nightly]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - uses: rhysd/action-setup-vim@v1
+ with:
+ neovim: true
+ version: ${{ matrix.nvim_version }}
+ - name: Run Test Cases
+ run: make test
# release:
# name: Release
# if: ${{ github.ref == 'refs/heads/main' }}
diff --git a/.luacov b/.luacov
deleted file mode 100644
index 0526f5bd..00000000
--- a/.luacov
+++ /dev/null
@@ -1,10 +0,0 @@
-modules = {
- ["colorbox"] = "lua/colorbox.lua",
- ["colorbox.*"] = "lua",
-}
-exclude = {
- "lua/colorbox/commons/*.lua",
- "lua/colorbox/commons/*/*.lua",
- "lua/colorbox/commons/*/*/*.lua",
-}
-
diff --git a/.luarc.json b/.luarc.json
index e6bbd6ee..495a0675 100644
--- a/.luarc.json
+++ b/.luarc.json
@@ -12,10 +12,12 @@
"inject-field",
"deprecated",
"luadoc-miss-module-name",
+ "luadoc-miss-symbol",
"undefined-doc-name",
"lowercase-global",
"cast-local-type",
- "param-type-mismatch"
+ "param-type-mismatch",
+ "missing-return"
],
"runtime.version": "LuaJIT",
"workspace.checkThirdParty": "Disable"
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..756fdb3e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,12 @@
+TESTS_INIT=spec_init.lua
+TESTS_DIR=spec/
+
+.PHONY: test
+
+test:
+ @nvim \
+ --headless \
+ --noplugin \
+ -u ${TESTS_INIT} \
+ -c "PlenaryBustedDirectory ${TESTS_DIR} { minimal_init = '${TESTS_INIT}' }"
+
diff --git a/README.md b/README.md
index cdbe4b14..e25e1c59 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,7 @@
-
-
-
-
Do you want all the **most popular** (Neo)Vim colorschemes than only one? Do you want to change them from time to time?
diff --git a/codecov.yml b/codecov.yml
deleted file mode 100644
index 15d1aa79..00000000
--- a/codecov.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-coverage:
- status:
- project:
- default:
- threshold: 90%
- patch:
- default:
- threshold: 90%
-ignore:
- - "lua/colorbox/commons/*.lua"
- - "lua/colorbox/commons/**/*.lua"
- - "lua/colorbox/commons/**/**/*.lua"
diff --git a/lua/colorbox.lua b/lua/colorbox.lua
index 4d097561..6d46fcd6 100644
--- a/lua/colorbox.lua
+++ b/lua/colorbox.lua
@@ -1,7 +1,5 @@
-local logging = require("colorbox.commons.logging")
-local LogLevels = require("colorbox.commons.logging").LogLevels
+local log = require("colorbox.commons.log")
local str = require("colorbox.commons.str")
-local tbl = require("colorbox.commons.tbl")
local configs = require("colorbox.configs")
local timing = require("colorbox.timing")
@@ -13,16 +11,13 @@ local loader = require("colorbox.loader")
--- @param opts colorbox.Options?
local function setup(opts)
local confs = configs.setup(opts)
-
- if not logging.has("colorbox") then
- logging.setup({
- name = "colorbox",
- level = confs.debug and LogLevels.DEBUG or LogLevels.INFO,
- console_log = confs.console_log,
- file_log = confs.file_log,
- file_log_name = "colorbox.log",
- })
- end
+ log.setup({
+ name = "colorbox",
+ 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 = "colorbox.log",
+ })
-- cache
assert(
@@ -37,8 +32,6 @@ local function setup(opts)
runtime.setup()
vim.api.nvim_create_user_command(confs.command.name, function(command_opts)
- local logger = logging.get("colorbox") --[[@as commons.logging.Logger]]
-
-- logger.debug(
-- "|colorbox.setup| command opts:%s",
-- vim.inspect(command_opts)
@@ -51,7 +44,7 @@ local function setup(opts)
-- vim.inspect(args_splits)
-- )
if #args_splits == 0 then
- logger:warn("missing parameter.")
+ log.warn("missing parameter.")
return
end
if vim.is_callable(controller[args_splits[1]]) then
@@ -59,7 +52,7 @@ local function setup(opts)
local sub_args = args:sub(string.len(args_splits[1]) + 1)
fn(sub_args)
else
- logger:warn(string.format("unknown parameter %s.", args_splits[1]))
+ log.warn(string.format("unknown parameter %s.", args_splits[1]))
end
end, {
nargs = "*",
@@ -80,7 +73,7 @@ local function setup(opts)
vim.api.nvim_create_autocmd("ColorSchemePre", {
callback = function(event)
- loader.load(tbl.tbl_get(event, "match"), false)
+ loader.load(vim.tbl_get(event, "match"), false)
end,
})
diff --git a/lua/colorbox/commons/async.lua b/lua/colorbox/commons/async.lua
index aa1743c9..bfc7d5ef 100644
--- a/lua/colorbox/commons/async.lua
+++ b/lua/colorbox/commons/async.lua
@@ -1,95 +1,1772 @@
--- Copied from:
+---@diagnostic disable
+local util = require('colorbox.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/colorbox/commons/async/_util.lua b/lua/colorbox/commons/async/_util.lua
new file mode 100644
index 00000000..b03bfa53
--- /dev/null
+++ b/lua/colorbox/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/colorbox/commons/async/misc.lua b/lua/colorbox/commons/async/misc.lua
new file mode 100644
index 00000000..80b599b9
--- /dev/null
+++ b/lua/colorbox/commons/async/misc.lua
@@ -0,0 +1,92 @@
+---@diagnostic disable
+local async = require('colorbox.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/colorbox/commons/color/hl.lua b/lua/colorbox/commons/color/hl.lua
deleted file mode 100644
index 4f04d4bd..00000000
--- a/lua/colorbox/commons/color/hl.lua
+++ /dev/null
@@ -1,57 +0,0 @@
-local M = {}
-
---- @param hl string
---- @return {fg:integer?,bg:integer?,[string]:any,ctermfg:integer?,ctermbg:integer?,cterm:{fg:integer?,bg:integer?,[string]:any}?}
-M.get_hl = function(hl)
- return vim.api.nvim_get_hl(0, { name = hl, link = false })
-end
-
---- @param ... string?
---- @return {fg:integer?,bg:integer?,[string]:any,ctermfg:integer?,ctermbg:integer?,cterm:{fg:integer?,bg:integer?,[string]:any}?}, integer, string?
-M.get_hl_with_fallback = function(...)
- for i, hl in ipairs({ ... }) do
- if type(hl) == "string" then
- local hl_value = M.get_hl(hl)
- if type(hl_value) == "table" and not vim.tbl_isempty(hl_value) then
- return hl_value, i, hl
- end
- end
- end
-
- return vim.empty_dict(), -1, nil
-end
-
---- @param highlight string
---- @param attr "fg"|"bg"|string
---- @return string?
-M.get_color = function(highlight, attr)
- assert(type(highlight) == "string")
- assert(attr == "fg" or attr == "bg" or attr == "sp")
-
- local hl_value = M.get_hl(highlight)
- if type(hl_value) == "table" and type(hl_value[attr]) == "number" then
- return string.format("#%06x", hl_value[attr])
- end
- return nil
-end
-
---- @param highlights string|string[]
---- @param attr "fg"|"bg"|string
---- @param fallback_color string?
---- @return string?, integer, string?
-M.get_color_with_fallback = function(highlights, attr, fallback_color)
- assert(type(highlights) == "string" or type(highlights) == "table")
- assert(type(attr) == "string")
- local hls = type(highlights) == "string" and { highlights } or highlights --[[@as table]]
-
- for i, hl in ipairs(hls) do
- local hl_value = M.get_hl(hl)
- if type(hl_value) == "table" and type(hl_value[attr]) == "number" then
- return string.format("#%06x", hl_value[attr]), i, hl
- end
- end
-
- return fallback_color, -1, nil
-end
-
-return M
diff --git a/lua/colorbox/commons/color/hsl.lua b/lua/colorbox/commons/color/hsl.lua
deleted file mode 100644
index 1eb27033..00000000
--- a/lua/colorbox/commons/color/hsl.lua
+++ /dev/null
@@ -1,271 +0,0 @@
----@diagnostic disable
------------------------------------------------------------------------------
--- Provides support for color manipulation in HSL color space.
---
--- http://sputnik.freewisdom.org/lib/colors/
---
--- License: MIT/X
---
--- (c) 2008 Yuri Takhteyev (yuri@freewisdom.org) *
---
--- * rgb_to_hsl() implementation was contributed by Markus Fleck-Graffe.
------------------------------------------------------------------------------
-
-module(..., package.seeall)
-
-local Color = {}
-local Color_mt = {__metatable = {}, __index = Color}
-
------------------------------------------------------------------------------
--- Instantiates a new "color".
---
--- @param H hue (0-360) _or_ an RGB string ("#930219")
--- @param S saturation (0.0-1.0)
--- @param L lightness (0.0-1.0)
--- @return an instance of Color
------------------------------------------------------------------------------
-function new(H, S, L)
- if type(H) == "string" and H:sub(1,1)=="#" and H:len() == 7 then
- H, S, L = rgb_string_to_hsl(H)
- end
- assert(Color_mt)
- return setmetatable({H = H, S = S, L = L}, Color_mt)
-end
-
------------------------------------------------------------------------------
--- Converts an HSL triplet to RGB
--- (see http://homepages.cwi.nl/~steven/css/hsl.html).
---
--- @param h hue (0-360)
--- @param s saturation (0.0-1.0)
--- @param L lightness (0.0-1.0)
--- @return an R, G, and B component of RGB
------------------------------------------------------------------------------
-
-function hsl_to_rgb(h, s, L)
- h = h/360
- local m1, m2
- if L<=0.5 then
- m2 = L*(s+1)
- else
- m2 = L+s-L*s
- end
- m1 = L*2-m2
-
- local function _h2rgb(m1, m2, h)
- if h<0 then h = h+1 end
- if h>1 then h = h-1 end
- if h*6<1 then
- return m1+(m2-m1)*h*6
- elseif h*2<1 then
- return m2
- elseif h*3<2 then
- return m1+(m2-m1)*(2/3-h)*6
- else
- return m1
- end
- end
-
- return _h2rgb(m1, m2, h+1/3), _h2rgb(m1, m2, h), _h2rgb(m1, m2, h-1/3)
-end
-
------------------------------------------------------------------------------
--- Converts an RGB triplet to HSL.
--- (see http://easyrgb.com)
---
--- @param r red (0.0-1.0)
--- @param g green (0.0-1.0)
--- @param b blue (0.0-1.0)
--- @return corresponding H, S and L components
------------------------------------------------------------------------------
-
-function rgb_to_hsl(r, g, b)
- --r, g, b = r/255, g/255, b/255
- local min = math.min(r, g, b)
- local max = math.max(r, g, b)
- local delta = max - min
-
- local h, s, l = 0, 0, ((min+max)/2)
-
- if l > 0 and l < 0.5 then s = delta/(max+min) end
- if l >= 0.5 and l < 1 then s = delta/(2-max-min) end
-
- if delta > 0 then
- if max == r and max ~= g then h = h + (g-b)/delta end
- if max == g and max ~= b then h = h + 2 + (b-r)/delta end
- if max == b and max ~= r then h = h + 4 + (r-g)/delta end
- h = h / 6;
- end
-
- if h < 0 then h = h + 1 end
- if h > 1 then h = h - 1 end
-
- return h * 360, s, l
-end
-
-function rgb_string_to_hsl(rgb)
- return rgb_to_hsl(tonumber(rgb:sub(2,3), 16)/256,
- tonumber(rgb:sub(4,5), 16)/256,
- tonumber(rgb:sub(6,7), 16)/256)
-end
-
------------------------------------------------------------------------------
--- Converts the color to an RGB string.
---
--- @return a 6-digit RGB representation of the color prefixed
--- with "#" (suitable for inclusion in HTML)
------------------------------------------------------------------------------
-
-function Color:to_rgb()
- local r, g, b = hsl_to_rgb(self.H, self.S, self.L)
- local rgb = {hsl_to_rgb(self.H, self.S, self.L)}
- local buffer = "#"
- for i,v in ipairs(rgb) do
- buffer = buffer..string.format("%02x",math.floor(v*256+0.5))
- end
- return buffer
-end
-
------------------------------------------------------------------------------
--- Creates a new color with hue different by delta.
---
--- @param delta a delta for hue.
--- @return a new instance of Color.
------------------------------------------------------------------------------
-function Color:hue_offset(delta)
- return new((self.H + delta) % 360, self.S, self.L)
-end
-
------------------------------------------------------------------------------
--- Creates a complementary color.
---
--- @return a new instance of Color
------------------------------------------------------------------------------
-function Color:complementary()
- return self:hue_offset(180)
-end
-
------------------------------------------------------------------------------
--- Creates two neighboring colors (by hue), offset by "angle".
---
--- @param angle the difference in hue between this color and the
--- neighbors
--- @return two new instances of Color
------------------------------------------------------------------------------
-function Color:neighbors(angle)
- local angle = angle or 30
- return self:hue_offset(angle), self:hue_offset(360-angle)
-end
-
------------------------------------------------------------------------------
--- Creates two new colors to make a triadic color scheme.
---
--- @return two new instances of Color
------------------------------------------------------------------------------
-function Color:triadic()
- return self:neighbors(120)
-end
-
------------------------------------------------------------------------------
--- Creates two new colors, offset by angle from this colors complementary.
---
--- @param angle the difference in hue between the complementary and
--- the returned colors
--- @return two new instances of Color
------------------------------------------------------------------------------
-function Color:split_complementary(angle)
- return self:neighbors(180-(angle or 30))
-end
-
------------------------------------------------------------------------------
--- Creates a new color with saturation set to a new value.
---
--- @param saturation the new saturation value (0.0 - 1.0)
--- @return a new instance of Color
------------------------------------------------------------------------------
-function Color:desaturate_to(saturation)
- return new(self.H, saturation, self.L)
-end
-
------------------------------------------------------------------------------
--- Creates a new color with saturation set to a old saturation times r.
---
--- @param r the multiplier for the new saturation
--- @return a new instance of Color
------------------------------------------------------------------------------
-function Color:desaturate_by(r)
- return new(self.H, self.S*r, self.L)
-end
-
------------------------------------------------------------------------------
--- Creates a new color with lightness set to a new value.
---
--- @param lightness the new lightness value (0.0 - 1.0)
--- @return a new instance of Color
------------------------------------------------------------------------------
-function Color:lighten_to(lightness)
- return new(self.H, self.S, lightness)
-end
-
------------------------------------------------------------------------------
--- Creates a new color with lightness set to a old lightness times r.
---
--- @param r the multiplier for the new lightness
--- @return a new instance of Color
------------------------------------------------------------------------------
-function Color:lighten_by(r)
- return new(self.H, self.S, self.L*r)
-end
-
------------------------------------------------------------------------------
--- Creates n variations of this color using supplied function and returns
--- them as a table.
---
--- @param f the function to create variations
--- @param n the number of variations
--- @return a table with n values containing the new colors
------------------------------------------------------------------------------
-function Color:variations(f, n)
- n = n or 5
- local results = {}
- for i=1,n do
- table.insert(results, f(self, i, n))
- end
- return results
-end
-
------------------------------------------------------------------------------
--- Creates n tints of this color and returns them as a table
---
--- @param n the number of tints
--- @return a table with n values containing the new colors
------------------------------------------------------------------------------
-function Color:tints(n)
- local f = function (color, i, n)
- return color:lighten_to(color.L + (1-color.L)/n*i)
- end
- return self:variations(f, n)
-end
-
------------------------------------------------------------------------------
--- Creates n shades of this color and returns them as a table
---
--- @param n the number of shades
--- @return a table with n values containing the new colors
------------------------------------------------------------------------------
-function Color:shades(n)
- local f = function (color, i, n)
- return color:lighten_to(color.L - (color.L)/n*i)
- end
- return self:variations(f, n)
-end
-
-function Color:tint(r)
- return self:lighten_to(self.L + (1-self.L)*r)
-end
-
-function Color:shade(r)
- return self:lighten_to(self.L - self.L*r)
-end
-
-Color_mt.__tostring = Color.to_rgb
diff --git a/lua/colorbox/commons/color/term.lua b/lua/colorbox/commons/color/term.lua
deleted file mode 100644
index 8a73d3e0..00000000
--- a/lua/colorbox/commons/color/term.lua
+++ /dev/null
@@ -1,114 +0,0 @@
-local M = {}
-
---- @package
---- @param attr "fg"|"bg"
---- @param code string
---- @return string
-M.escape = function(attr, code)
- assert(type(code) == "string")
- assert(attr == "bg" or attr == "fg")
-
- local control = attr == "fg" and 38 or 48
- local r, g, b = code:match("#(..)(..)(..)")
- if r and g and b then
- r = tonumber(r, 16)
- g = tonumber(g, 16)
- b = tonumber(b, 16)
- return string.format("%d;2;%d;%d;%d", control, r, g, b)
- else
- return string.format("%d;5;%s", control, code)
- end
-end
-
--- Pre-defined CSS colors
--- Also see: https://www.quackit.com/css/css_color_codes.cfm
-local CSS_COLORS = {
- black = "0;30",
- grey = M.escape("fg", "#808080"),
- silver = M.escape("fg", "#c0c0c0"),
- white = M.escape("fg", "#ffffff"),
- violet = M.escape("fg", "#EE82EE"),
- magenta = "0;35",
- fuchsia = M.escape("fg", "#FF00FF"),
- red = "0;31",
- purple = M.escape("fg", "#800080"),
- indigo = M.escape("fg", "#4B0082"),
- yellow = "0;33",
- gold = M.escape("fg", "#FFD700"),
- orange = M.escape("fg", "#FFA500"),
- chocolate = M.escape("fg", "#D2691E"),
- olive = M.escape("fg", "#808000"),
- green = "0;32",
- lime = M.escape("fg", "#00FF00"),
- teal = M.escape("fg", "#008080"),
- cyan = "0;36",
- aqua = M.escape("fg", "#00FFFF"),
- blue = "0;34",
- navy = M.escape("fg", "#000080"),
- slateblue = M.escape("fg", "#6A5ACD"),
- steelblue = M.escape("fg", "#4682B4"),
-}
-
---- @param text string the text content to be rendered
---- @param name string the ANSI color name or RGB color codes
---- @param hl string? the highlighting group name
---- @return string
-M.render = function(text, name, hl)
- local str = require("colorbox.commons.str")
- local color_hl = require("colorbox.commons.color.hl")
-
- local fgfmt = nil
- local hlcodes = str.not_empty(hl) and color_hl.get_hl(hl --[[@as string]]) or nil
- local fgcode = type(hlcodes) == "table" and hlcodes.fg or nil
- if type(fgcode) == "number" then
- fgfmt = M.escape("fg", string.format("#%06x", fgcode))
- elseif CSS_COLORS[name] then
- fgfmt = CSS_COLORS[name]
- else
- fgfmt = M.escape("fg", name)
- end
-
- local fmt = nil
- local bgcode = type(hlcodes) == "table" and hlcodes.bg or nil
- if type(bgcode) == "number" then
- local bgcolor = M.escape("bg", string.format("#%06x", bgcode))
- fmt = string.format("%s;%s", fgfmt, bgcolor)
- else
- fmt = fgfmt
- end
- return string.format("[%sm%s[0m", fmt, text)
-end
-
---- @param text string?
---- @return string?
-M.erase = function(text)
- assert(type(text) == "string")
-
- local result, pos = text
- :gsub("\x1b%[%d+m\x1b%[K", "")
- :gsub("\x1b%[m\x1b%[K", "")
- :gsub("\x1b%[%d+;%d+;%d+;%d+;%d+m", "")
- :gsub("\x1b%[%d+;%d+;%d+;%d+m", "")
- :gsub("\x1b%[%d+;%d+;%d+m", "")
- :gsub("\x1b%[%d+;%d+m", "")
- :gsub("\x1b%[%d+m", "")
- :gsub("\x1b%[m", "")
- return result
-end
-
---- @type string[]
-M.COLOR_NAMES = {}
-
-do
- for name, code in pairs(CSS_COLORS) do
- --- @param text string
- --- @param hl string?
- --- @return string
- M[name] = function(text, hl)
- return M.render(text, name, hl)
- end
- table.insert(M.COLOR_NAMES, name)
- end
-end
-
-return M
diff --git a/lua/colorbox/commons/fio.lua b/lua/colorbox/commons/fio.lua
index 136fb699..8118a865 100644
--- a/lua/colorbox/commons/fio.lua
+++ b/lua/colorbox/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("colorbox.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("colorbox.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/colorbox/commons/log.lua b/lua/colorbox/commons/log.lua
new file mode 100644
index 00000000..f32a2592
--- /dev/null
+++ b/lua/colorbox/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/colorbox/commons/logging.lua b/lua/colorbox/commons/logging.lua
deleted file mode 100644
index 06455cd7..00000000
--- a/lua/colorbox/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("colorbox.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/colorbox/commons/num.lua b/lua/colorbox/commons/num.lua
index 1c076873..f7d668d9 100644
--- a/lua/colorbox/commons/num.lua
+++ b/lua/colorbox/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/colorbox/commons/path.lua b/lua/colorbox/commons/path.lua
index 7912b9e5..f476d041 100644
--- a/lua/colorbox/commons/path.lua
+++ b/lua/colorbox/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("colorbox.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/colorbox/commons/platform.lua b/lua/colorbox/commons/platform.lua
index 8c2ca636..a588f47d 100644
--- a/lua/colorbox/commons/platform.lua
+++ b/lua/colorbox/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/colorbox/commons/spawn.lua b/lua/colorbox/commons/spawn.lua
deleted file mode 100644
index 838c3f27..00000000
--- a/lua/colorbox/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("colorbox.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/colorbox/commons/str.lua b/lua/colorbox/commons/str.lua
index 72e2f090..26c8fe94 100644
--- a/lua/colorbox/commons/str.lua
+++ b/lua/colorbox/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("colorbox.commons.tbl").list_index(pos, n)
local buffer = ""
if pos > 1 then
diff --git a/lua/colorbox/commons/tbl.lua b/lua/colorbox/commons/tbl.lua
index c91ef17a..82acef5b 100644
--- a/lua/colorbox/commons/tbl.lua
+++ b/lua/colorbox/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/colorbox/commons/uv.lua b/lua/colorbox/commons/uv.lua
deleted file mode 100644
index 8a70d613..00000000
--- a/lua/colorbox/commons/uv.lua
+++ /dev/null
@@ -1 +0,0 @@
-return vim.uv or vim.loop
diff --git a/lua/colorbox/commons/version.lua b/lua/colorbox/commons/version.lua
deleted file mode 100644
index e44c0490..00000000
--- a/lua/colorbox/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/colorbox/commons/version.txt b/lua/colorbox/commons/version.txt
deleted file mode 100644
index 008c39a4..00000000
--- a/lua/colorbox/commons/version.txt
+++ /dev/null
@@ -1 +0,0 @@
-27.0.0
diff --git a/lua/colorbox/controller.lua b/lua/colorbox/controller.lua
index 2c1f9863..b1fac951 100644
--- a/lua/colorbox/controller.lua
+++ b/lua/colorbox/controller.lua
@@ -1,9 +1,7 @@
local num = require("colorbox.commons.num")
local str = require("colorbox.commons.str")
-local logging = require("colorbox.commons.logging")
-local LogLevels = require("colorbox.commons.logging").LogLevels
+local log = require("colorbox.commons.log")
local async = require("colorbox.commons.async")
-local spawn = require("colorbox.commons.spawn")
local loader = require("colorbox.loader")
@@ -13,21 +11,14 @@ local db = require("colorbox.db")
local M = {}
M.update = function()
- if not logging.has("colorbox-update") then
- logging.setup({
- name = "colorbox-update",
- level = LogLevels.DEBUG,
- console_log = true,
- file_log = true,
- file_log_name = "colorbox_update.log",
- file_log_mode = "w",
- })
- end
- local logger = logging.get("colorbox-update")
+ log.setup({
+ name = "colorbox-update",
+ level = vim.log.levels.DEBUG,
+ })
local home_dir = vim.fn["colorbox#base_dir"]()
local packstart = string.format("%s/pack/colorbox/start", home_dir)
- logger:debug(
+ log.debug(
string.format(
"|colorbox.init| home_dir:%s, packstart:%s",
vim.inspect(home_dir),
@@ -42,23 +33,24 @@ M.update = function()
for _, _ in pairs(HandleToColorSpecsMap) do
prepared_count = prepared_count + 1
end
- logger:info(string.format("started %s jobs", vim.inspect(prepared_count)))
+ log.info(string.format("started %s jobs", vim.inspect(prepared_count)))
- local async_run = async.wrap(function(cmd, opts, cb)
- spawn.detached(cmd, opts, function(result)
- cb(result)
- end)
- end, 3)
-
- async.void(function()
+ async.run(function()
local finished_count = 0
for handle, spec in pairs(HandleToColorSpecsMap) do
- local function _on_output(line)
- if str.not_blank(line) then
- logger:info(string.format("%s: %s", handle, line))
+ local buffer = nil
+ local function _on_output(err, data)
+ if data then
+ buffer = buffer and (buffer .. data) or data
+ end
+ end
+ local function _on_exit()
+ if str.not_empty(buffer) then
+ log.info(string.format("%s: %s", handle, buffer))
end
end
+
local pack_path = db.get_pack_path(spec)
local full_pack_path = db.get_full_pack_path(spec)
local param = nil
@@ -70,8 +62,10 @@ M.update = function()
cmd = { "git", "pull" },
opts = {
cwd = full_pack_path,
- on_stdout = _on_output,
- on_stderr = _on_output,
+ stdout = _on_output,
+ stderr = _on_output,
+ on_exit = _on_exit,
+ text = true,
},
}
-- logger:debug(
@@ -100,8 +94,10 @@ M.update = function()
},
opts = {
cwd = home_dir,
- on_stdout = _on_output,
- on_stderr = _on_output,
+ stdout = _on_output,
+ stderr = _on_output,
+ on_exit = _on_exit,
+ text = true,
},
}
-- logger:debug(
@@ -112,20 +108,27 @@ M.update = function()
-- )
-- )
end
- async_run(param.cmd, param.opts)
- async.schedule()
+
+ async.wrap(1, function(cb)
+ vim.system(param.cmd, param.opts, function(result)
+ param.opts.on_exit()
+ cb(result)
+ end)
+ end)()
+
+ async.await(1, vim.schedule)
finished_count = finished_count + 1
end
- logger:info(string.format("finished %s jobs", vim.inspect(finished_count)))
- end)()
+ log.info(string.format("finished %s jobs", vim.inspect(finished_count)))
+ end)
end
--- @param args string?
--- @return colorbox.Options?
M._parse_args = function(args)
local opts = nil
- logging.get("colorbox"):debug(string.format("|_parse_args| args:%s", vim.inspect(args)))
+ log.debug(string.format("|_parse_args| args:%s", vim.inspect(args)))
if str.not_blank(args) then
local args_splits = str.split(vim.trim(args --[[@as string]]), " ", { trimempty = true })
for _, arg_split in ipairs(args_splits) do
@@ -144,11 +147,10 @@ end
M.shuffle = function()
local ColorNamesList = runtime.colornames()
if #ColorNamesList > 0 then
- local logger = logging.get("colorbox")
local random_index = num.random(1, #ColorNamesList)
local color = ColorNamesList[random_index]
- logger:debug(
+ log.debug(
string.format(
"|shuffle| color:%s, random_index:%d, ColorNamesList(%d):%s",
vim.inspect(color),
@@ -163,12 +165,11 @@ end
--- @param args string?
M.info = function(args)
- local logger = logging.get("colorbox")
local opts = M._parse_args(args)
opts = opts or { scale = 0.7 }
opts.scale = type(opts.scale) == "string" and (tonumber(opts.scale) or 0.7) or 0.7
- opts.scale = num.bound(opts.scale, 0, 1)
- logging.get("colorbox"):debug(string.format("|_info| opts:%s", vim.inspect(opts)))
+ opts.scale = num.clamp(opts.scale, 0, 1)
+ log.debug(string.format("|_info| opts:%s", vim.inspect(opts)))
local total_width = vim.o.columns
local total_height = vim.o.lines
@@ -178,7 +179,7 @@ M.info = function(args)
local function get_shift(totalsize, modalsize, offset)
local base = math.floor((totalsize - modalsize) * 0.5)
local shift = offset > -1 and math.ceil((totalsize - modalsize) * offset) or offset
- return num.bound(base + shift, 0, totalsize - modalsize)
+ return num.clamp(base + shift, 0, totalsize - modalsize)
end
local row = get_shift(total_height, height, 0)
diff --git a/lua/colorbox/filter.lua b/lua/colorbox/filter.lua
index ac4fe82b..d93132d8 100644
--- a/lua/colorbox/filter.lua
+++ b/lua/colorbox/filter.lua
@@ -1,4 +1,4 @@
-local logging = require("colorbox.commons.logging")
+local log = require("colorbox.commons.log")
local configs = require("colorbox.configs")
local builtin_filters = require("colorbox.filter.builtin")
@@ -27,7 +27,7 @@ M._function_filter = function(f, color_name, spec)
if ok and type(result) == "boolean" then
return result
else
- logging.get("colorbox"):err("failed to invoke function filter, please check your config!")
+ log.err("failed to invoke function filter, please check your config!")
end
end
return false
diff --git a/lua/colorbox/filter/builtin.lua b/lua/colorbox/filter/builtin.lua
index 52a626e8..44364bba 100644
--- a/lua/colorbox/filter/builtin.lua
+++ b/lua/colorbox/filter/builtin.lua
@@ -1,6 +1,5 @@
local num = require("colorbox.commons.num")
local str = require("colorbox.commons.str")
-local logging = require("colorbox.commons.logging")
local M = {}
diff --git a/lua/colorbox/loader.lua b/lua/colorbox/loader.lua
index b5c384d3..8d691bf7 100644
--- a/lua/colorbox/loader.lua
+++ b/lua/colorbox/loader.lua
@@ -1,8 +1,8 @@
local str = require("colorbox.commons.str")
local tbl = require("colorbox.commons.tbl")
-local uv = require("colorbox.commons.uv")
-local logging = require("colorbox.commons.logging")
+local log = require("colorbox.commons.log")
+local uv = vim.uv or vim.loop
local configs = require("colorbox.configs")
local track = require("colorbox.track")
local db = require("colorbox.db")
@@ -12,7 +12,6 @@ local M = {}
--- @param colorname string?
--- @param execute boolean?
M.load = function(colorname, execute)
- local logger = logging.get("colorbox")
local ColorNameToColorSpecsMap = require("colorbox.db").get_color_name_to_color_specs_map()
if str.empty(colorname) then
@@ -24,7 +23,7 @@ M.load = function(colorname, execute)
end
local full_pack_path = db.get_full_pack_path(spec)
local pack_exist = uv.fs_stat(full_pack_path) ~= nil
- logger:debug(
+ log.debug(
string.format(
"|load| full_pack_path:%s, pack_exist:%s",
vim.inspect(full_pack_path),
@@ -43,7 +42,7 @@ M.load = function(colorname, execute)
if type(confs.setup) == "table" and vim.is_callable(confs.setup[spec.handle]) then
local home_dir = vim.fn["colorbox#base_dir"]()
local ok, setup_err = pcall(confs.setup[spec.handle], home_dir, spec)
- logger:ensure(
+ log.ensure(
ok,
string.format(
"failed to setup colorscheme:%s, error:%s",
diff --git a/lua/colorbox/policy/builtin.lua b/lua/colorbox/policy/builtin.lua
index 11cfc201..86d86fb1 100644
--- a/lua/colorbox/policy/builtin.lua
+++ b/lua/colorbox/policy/builtin.lua
@@ -1,5 +1,5 @@
local num = require("colorbox.commons.num")
-local logging = require("colorbox.commons.logging")
+local log = require("colorbox.commons.log")
local runtime = require("colorbox.runtime")
local track = require("colorbox.track")
@@ -12,7 +12,7 @@ M.shuffle = function()
if #ColorNamesList > 0 then
local i = num.random(#ColorNamesList) --[[@as integer]]
local color = track.get_next_color_name_by_idx(i)
- logging.get("colorbox"):debug(
+ log.debug(
string.format(
"|_policy_shuffle| color:%s, ColorNamesList:%s (%d), i:%d",
vim.inspect(color),
@@ -29,11 +29,10 @@ end
M.in_order = function()
local ColorNamesList = runtime.colornames()
if #ColorNamesList > 0 then
- local logger = logging.get("colorbox")
local previous_track = track.previous_track() --[[@as colorbox.PreviousTrack]]
local i = previous_track ~= nil and previous_track.color_number or 0
local color = track.get_next_color_name_by_idx(i)
- logger:debug(
+ log.debug(
string.format(
"|in_order| color:%s, i:%d, ColorNamesList(%d):%s",
vim.inspect(color),
diff --git a/lua/colorbox/policy/fixed_interval.lua b/lua/colorbox/policy/fixed_interval.lua
index a12371ce..3da2c926 100644
--- a/lua/colorbox/policy/fixed_interval.lua
+++ b/lua/colorbox/policy/fixed_interval.lua
@@ -1,7 +1,6 @@
local num = require("colorbox.commons.num")
local str = require("colorbox.commons.str")
-local tbl = require("colorbox.commons.tbl")
-local logging = require("colorbox.commons.logging")
+local log = require("colorbox.commons.log")
local configs = require("colorbox.configs")
local builtin_policy = require("colorbox.policy.builtin")
@@ -17,19 +16,18 @@ end
M.run = function()
local confs = configs.get()
- local logger = logging.get("colorbox") --[[@as commons.logging.Logger]]
- logger:ensure(
+ log.ensure(
M.is_fixed_interval_policy(confs.policy),
string.format("invalid policy %s for 'interval' timing!", vim.inspect(confs.policy))
)
local later = confs.policy.seconds > 0 and (confs.policy.seconds * 1000) or num.INT32_MAX
- local implement_policy = tbl.tbl_get(confs, "policy", "implement")
- logger:ensure(
+ local implement_policy = vim.tbl_get(confs, "policy", "implement")
+ log.ensure(
not str.empty(implement_policy),
string.format("invalid policy %s for 'interval' timing!", vim.inspect(confs.policy))
)
local fn = builtin_policy[implement_policy]
- logger:ensure(
+ log.ensure(
vim.is_callable(fn),
string.format("invalid policy %s for 'interval' timing!", vim.inspect(confs.policy))
)
diff --git a/lua/colorbox/runtime.lua b/lua/colorbox/runtime.lua
index 65dd33cf..f0395fb0 100644
--- a/lua/colorbox/runtime.lua
+++ b/lua/colorbox/runtime.lua
@@ -1,9 +1,9 @@
local tbl = require("colorbox.commons.tbl")
local str = require("colorbox.commons.str")
-local uv = require("colorbox.commons.uv")
local fio = require("colorbox.commons.fio")
-local logging = require("colorbox.commons.logging")
+local log = require("colorbox.commons.log")
+local uv = vim.uv or vim.loop
local configs = require("colorbox.configs")
local filter = require("colorbox.filter")
local db = require("colorbox.db")
@@ -22,8 +22,6 @@ local FilteredColorNameToIndexMap = {}
M._build_colors = function()
local colors_list = {}
local colors_index = {}
- local logger = logging.get("colorbox")
-
local ColorNameToColorSpecsMap = require("colorbox.db").get_color_name_to_color_specs_map()
local ColorNamesList = require("colorbox.db").get_color_names_list()
for _, color_name in pairs(ColorNamesList) do
@@ -55,25 +53,24 @@ M._build_colors = function()
end
M.setup = function()
- local logger = logging.get("colorbox") --[[@as commons.logging.Logger]]
-
local home_dir = vim.fn["colorbox#base_dir"]()
vim.opt.packpath:append(home_dir)
local confs = configs.get()
- logger:debug(
+ log.debug(
string.format(
"|setup| confs.previous_colors_cache:%s",
vim.inspect(confs.previous_colors_cache)
)
)
- local cache_content = fio.readfile(confs.previous_colors_cache, { trim = true })
- logger:debug(string.format("|setup| cache_content:%s", vim.inspect(cache_content)))
+ local cache_content = fio.readfile(confs.previous_colors_cache)
+ cache_content = str.not_empty(cache_content) and str.trim(cache_content) or ""
+ log.debug(string.format("|setup| cache_content:%s", vim.inspect(cache_content)))
local found_cache = false
if cache_content then
local colors_list = str.split(cache_content, ",")
- logger:debug(string.format("|setup| colors_list:%s", vim.inspect(colors_list)))
+ log.debug(string.format("|setup| colors_list:%s", vim.inspect(colors_list)))
if tbl.list_not_empty(colors_list) then
FilteredColorNamesList = colors_list
@@ -85,31 +82,27 @@ M.setup = function()
vim.defer_fn(function()
local data = M._build_colors()
- fio.asyncwritefile(
- confs.previous_colors_cache,
- table.concat(data.colors_list, ","),
- function()
- logger:debug("|setup| found cache, update cache - done")
- end
- )
+ fio.asyncwritefile(confs.previous_colors_cache, table.concat(data.colors_list, ","), {
+ on_complete = function()
+ log.debug("|setup| found cache, update cache - done")
+ end,
+ })
end, 100)
end
end
if not found_cache then
local data = M._build_colors()
- logger:debug(string.format("|setup| not found, data:%s", vim.inspect(data)))
+ log.debug(string.format("|setup| not found, data:%s", vim.inspect(data)))
FilteredColorNamesList = data.colors_list
FilteredColorNameToIndexMap = data.colors_index
vim.defer_fn(function()
- fio.asyncwritefile(
- confs.previous_colors_cache,
- table.concat(FilteredColorNamesList, ","),
- function()
- logger:debug("|setup| not found cache, dump cache - done")
- end
- )
+ fio.asyncwritefile(confs.previous_colors_cache, table.concat(FilteredColorNamesList, ","), {
+ on_complete = function()
+ log.debug("|setup| not found cache, dump cache - done")
+ end,
+ })
end, 100)
end
diff --git a/lua/colorbox/track.lua b/lua/colorbox/track.lua
index 5d80e63f..97bd5bf4 100644
--- a/lua/colorbox/track.lua
+++ b/lua/colorbox/track.lua
@@ -1,7 +1,7 @@
local str = require("colorbox.commons.str")
local num = require("colorbox.commons.num")
local fio = require("colorbox.commons.fio")
-local logging = require("colorbox.commons.logging")
+local log = require("colorbox.commons.log")
local configs = require("colorbox.configs")
local runtime = require("colorbox.runtime")
@@ -18,9 +18,8 @@ end
--- @alias colorbox.PreviousTrack {color_name:string,color_number:integer}
--- @param color_name string
M.save_track = function(color_name)
- local logger = logging.get("colorbox")
if str.blank(color_name) then
- logger:debug(string.format("|save_track| color_name is blank:%s", vim.inspect(color_name)))
+ log.debug(string.format("|save_track| color_name is blank:%s", vim.inspect(color_name)))
return
end
@@ -35,22 +34,24 @@ M.save_track = function(color_name)
color_number = color_number,
}) --[[@as string]]
- fio.asyncwritefile(confs.previous_track_cache, content, function()
- logger:debug(
- string.format(
- "|save_track| finished save track for color_name:%s, color_number:%s",
- vim.inspect(color_name),
- vim.inspect(color_number)
+ fio.asyncwritefile(confs.previous_track_cache, content, {
+ on_complete = function()
+ log.debug(
+ string.format(
+ "|save_track| finished save track for color_name:%s, color_number:%s",
+ vim.inspect(color_name),
+ vim.inspect(color_number)
+ )
)
- )
- vim.schedule(function()
- if vim.is_callable(confs.post_hook) then
- local ColorNameToColorSpecsMap = db.get_color_name_to_color_specs_map()
- local color_spec = ColorNameToColorSpecsMap[color_name]
- confs.post_hook(color_name, color_spec)
- end
- end)
- end)
+ vim.schedule(function()
+ if vim.is_callable(confs.post_hook) then
+ local ColorNameToColorSpecsMap = db.get_color_name_to_color_specs_map()
+ local color_spec = ColorNameToColorSpecsMap[color_name]
+ confs.post_hook(color_name, color_spec)
+ end
+ end)
+ end,
+ })
end)
end
@@ -74,7 +75,7 @@ M.get_next_color_name_by_idx = function(idx)
if idx > n then
idx = 1
end
- idx = num.bound(idx, 1, n)
+ idx = num.clamp(idx, 1, n)
return ColorNamesList[idx], idx
end
@@ -88,7 +89,7 @@ M.get_prev_color_name_by_idx = function(idx)
if idx < 1 then
idx = n
end
- idx = num.bound(idx, 1, n)
+ idx = num.clamp(idx, 1, n)
return ColorNamesList[idx], idx
end
diff --git a/spec/colorbox/loader_spec.lua b/spec/colorbox/loader_spec.lua
index 6eec8feb..cad4e296 100644
--- a/spec/colorbox/loader_spec.lua
+++ b/spec/colorbox/loader_spec.lua
@@ -25,15 +25,15 @@ describe("colorbox.loader", function()
it("load", function()
local color_name_to_color_specs_map = db.get_color_name_to_color_specs_map()
- -- if not github_actions then
- local colors = runtime.colornames()
- for i, c in ipairs(colors) do
- local colorspec = color_name_to_color_specs_map[c]
- if not disabled_colorspecs[colorspec.handle] then
- loader.load(c)
+ if not github_actions then
+ local colors = runtime.colornames()
+ for i, c in ipairs(colors) do
+ local colorspec = color_name_to_color_specs_map[c]
+ if not disabled_colorspecs[colorspec.handle] then
+ loader.load(c)
+ end
end
end
- -- end
end)
end)
end)
diff --git a/spec/colorbox/runtime_spec.lua b/spec/colorbox/runtime_spec.lua
index fca1d522..47e1cde3 100644
--- a/spec/colorbox/runtime_spec.lua
+++ b/spec/colorbox/runtime_spec.lua
@@ -9,7 +9,6 @@ describe("colorbox.runtime", function()
vim.api.nvim_command("cd " .. cwd)
end)
- local tbl = require("colorbox.commons.tbl")
local runtime = require("colorbox.runtime")
require("colorbox").setup({
debug = true,
@@ -20,8 +19,8 @@ describe("colorbox.runtime", function()
it("test", function()
local actual = runtime._build_colors()
print(string.format("_build_colors:%s\n", vim.inspect(actual)))
- assert_eq(type(tbl.tbl_get(actual, "colors_list")), "table")
- assert_eq(type(tbl.tbl_get(actual, "colors_index")), "table")
+ assert_eq(type(vim.tbl_get(actual, "colors_list")), "table")
+ assert_eq(type(vim.tbl_get(actual, "colors_index")), "table")
for i, color in ipairs(actual.colors_list) do
assert_eq(actual.colors_index[color], i)
end
diff --git a/spec/colorbox/track_spec.lua b/spec/colorbox/track_spec.lua
index 986bb366..cad7ff4d 100644
--- a/spec/colorbox/track_spec.lua
+++ b/spec/colorbox/track_spec.lua
@@ -9,6 +9,7 @@ describe("colorbox.track", function()
vim.api.nvim_command("cd " .. cwd)
end)
+ local str = require("colorbox.commons.str")
local track = require("colorbox.track")
local runtime = require("colorbox.runtime")
require("colorbox").setup({
@@ -49,7 +50,7 @@ describe("colorbox.track", function()
vim.inspect(actual_idx)
)
)
- if actual then
+ if str.not_empty(actual) then
assert_true(string.len(actual) > 0)
if i < n then
assert_eq(actual_idx, i + 1)
@@ -72,7 +73,7 @@ describe("colorbox.track", function()
vim.inspect(actual_idx)
)
)
- if actual then
+ if str.not_empty(actual) then
assert_true(string.len(actual) > 0)
if i > 1 and i <= n then
assert_eq(actual_idx, i - 1)
diff --git a/spec_init.lua b/spec_init.lua
new file mode 100644
index 00000000..b4c8c23d
--- /dev/null
+++ b/spec_init.lua
@@ -0,0 +1,11 @@
+local plenary_dir = os.getenv("PLENARY_DIR") or "/tmp/plenary.nvim"
+local is_not_a_directory = vim.fn.isdirectory(plenary_dir) == 0
+if is_not_a_directory then
+ vim.fn.system({ "git", "clone", "https://github.com/nvim-lua/plenary.nvim", plenary_dir })
+end
+
+vim.opt.rtp:append(".")
+vim.opt.rtp:append(plenary_dir)
+
+vim.cmd("runtime plugin/plenary.vim")
+require("plenary.busted")