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 @@

require -commons.nvim - ci.yml -collect.yml -

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", 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")