diff --git a/README.md b/README.md
index f7cd3e0..f55dba0 100644
--- a/README.md
+++ b/README.md
@@ -1,43 +1,22 @@
-# Blink Download (blink.download)
-
-Neovim libary for downloading pre-built binaries for Rust based plugins. For a quick start, see the [neovim-lua-rust-template](https://github.com/Saghen/neovim-lua-rust-template).
-
-## Usage
-
-Add the following at the top level of your plugin:
-
-```lua
-local my_plugin = {}
-
-function my_plugin.setup()
- -- get the root directory of the plugin, by getting the relative path to this file
- -- for example, if this file is in `/lua/my_plugin/init.lua`, use `../../`
- local root_dir = vim.fn.resolve(debug.getinfo(1).source:match('@?(.*/)') .. '../../')
-
- require('blink.download').ensure_downloaded({
- -- omit this property to disable downloading
- -- i.e. https://github.com/Saghen/blink.delimiters/releases/download/v0.1.0/x86_64-unknown-linux-gnu.so
- download_url = function(version, system_triple, extension)
- return 'https://github.com/saghen/blink.delimiters/releases/download/' .. version .. '/' .. system_triple .. extension
- end,
-
- root_dir,
- output_dir = '/target/release',
- binary_name = 'blink_delimiters' -- excluding `lib` prefix
- }, function(err)
- if err then error(err) end
-
- local rust_module = require('blink_delimiters')
- end)
-end
-```
-
-
-Add the following to your `build.rs`. This deletes the `version` file created by the downloader, such that the downloader will accept the binary as-is.
-
-```rust
-fn main() {
- // delete existing version file created by downloader
- let _ = std::fs::remove_file("target/release/version");
-}
-```
+
+
Blink Lib (blink.lib)
+
+
+> [!WARNING]
+> Not ready for use
+
+**blink.lib** provides generic utilities for all other blink plugins, aka all the code I don't want to copy between my plugins :)
+
+## Roadmap
+
+- [x] `blink.lib.task`: Async
+- [x] `blink.lib.fs`: Filesystem APIs
+- [ ] `blink.lib.config`: Config module with validation (merge `vim.g/vim.b/setup()`, `enable()`, `is_enabled()`)
+- [ ] `blink.lib`: Utils (lazy_require, dedup, debounce, truncate, dedent, copy, slice, ...) with all other modules exported (lazily)
+- [ ] `blink.lib.log`: Logging to file and/or notifications
+- [ ] `blink.lib.download`: Binary downloader (e.g. downloading rust binaries)
+- [ ] `blink.lib.build`: Build system (e.g. building rust binaries)
+- [ ] `blink.lib.regex`: Regex
+- [ ] `blink.lib.git`: Git APIs using FFI
+- [ ] `blink.lib.http`: HTTP APIs using [`reqwest`](https://github.com/seanmonstar/reqwest)
+- [ ] `blink.lib.lsp`: In-process LSP client wrapper
diff --git a/lua/blink/download.lua b/lua/blink/download.lua
new file mode 100644
index 0000000..385591f
--- /dev/null
+++ b/lua/blink/download.lua
@@ -0,0 +1 @@
+return require('blink.lib.download')
diff --git a/lua/blink/download/files.lua b/lua/blink/download/files.lua
deleted file mode 100644
index 1cde433..0000000
--- a/lua/blink/download/files.lua
+++ /dev/null
@@ -1,149 +0,0 @@
-local async = require('blink.download.lib.async')
-
---- @class blink.download.Files
---- @field root_dir string
---- @field lib_folder string
---- @field lib_filename string
---- @field lib_path string
---- @field checksum_path string
---- @field checksum_filename string
---- @field version_path string
----
---- @field new fun(root_dir: string, output_dir: string, binary_name: string): blink.download.Files
----
---- @field get_version fun(self: blink.download.Files): blink.download.Task
---- @field set_version fun(self: blink.download.Files, version: string): blink.download.Task
----
---- @field get_lib_extension fun(): string Returns the extension for the library based on the current platform, including the dot (i.e. '.so' or '.dll')
----
---- @field read_file fun(path: string): blink.download.Task
---- @field write_file fun(path: string, data: string): blink.download.Task
---- @field exists fun(path: string): blink.download.Task
---- @field stat fun(path: string): blink.download.Task
---- @field create_dir fun(path: string): blink.download.Task
---- @field rename fun(old_path: string, new_path: string): blink.download.Task
-
---- @type blink.download.Files
---- @diagnostic disable-next-line: missing-fields
-local files = {}
-
-function files.new(root_dir, output_dir, binary_name)
- -- Normalize trailing and leading slashes
- if root_dir:sub(#root_dir, #root_dir) ~= '/' then root_dir = root_dir .. '/' end
- if output_dir:sub(1, 1) == '/' then output_dir = output_dir:sub(2) end
-
- local lib_folder = root_dir .. output_dir
- local lib_filename = 'lib' .. binary_name .. files.get_lib_extension()
- local lib_path = lib_folder .. '/' .. lib_filename
-
- local self = setmetatable({}, { __index = files })
-
- self.root_dir = root_dir
- self.lib_folder = lib_folder
- self.lib_filename = lib_filename
- self.lib_path = lib_path
- self.checksum_path = lib_path .. '.sha256'
- self.checksum_filename = lib_filename .. '.sha256'
- self.version_path = lib_folder .. '/version'
-
- return self
-end
-
---- Version file ---
-
-function files:get_version()
- return files
- .read_file(self.version_path)
- :map(function(version) return { tag = version } end)
- :catch(function() return { missing = true } end)
-end
-
---- @param version string
---- @return blink.download.Task
-function files:set_version(version)
- return files
- .create_dir(self.root_dir .. '/target')
- :map(function() return files.create_dir(self.lib_folder) end)
- :map(function() return files.write_file(self.version_path, version) end)
-end
-
---- Util ---
-
-function files.get_lib_extension()
- if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then return '.dylib' end
- if jit.os:lower() == 'windows' then return '.dll' end
- return '.so'
-end
-
---- Filesystem helpers ---
-
---- @param path string
---- @return blink.download.Task
-function files.read_file(path)
- return async.task.new(function(resolve, reject)
- vim.uv.fs_open(path, 'r', 438, function(open_err, fd)
- if open_err or fd == nil then return reject(open_err or 'Unknown error') end
- vim.uv.fs_read(fd, 1024, 0, function(read_err, data)
- vim.uv.fs_close(fd, function() end)
- if read_err or data == nil then return reject(read_err or 'Unknown error') end
- return resolve(data)
- end)
- end)
- end)
-end
-
-function files.write_file(path, data)
- return async.task.new(function(resolve, reject)
- vim.uv.fs_open(path, 'w', 438, function(open_err, fd)
- if open_err or fd == nil then return reject(open_err or 'Unknown error') end
- vim.uv.fs_write(fd, data, 0, function(write_err)
- vim.uv.fs_close(fd, function() end)
- if write_err then return reject(write_err) end
- return resolve()
- end)
- end)
- end)
-end
-
-function files.exists(path)
- return async.task.new(function(resolve)
- vim.uv.fs_stat(path, function(err) resolve(not err) end)
- end)
-end
-
-function files.stat(path)
- return async.task.new(function(resolve, reject)
- vim.uv.fs_stat(path, function(err, stat)
- if err then return reject(err) end
- resolve(stat)
- end)
- end)
-end
-
-function files.create_dir(path)
- return files
- .stat(path)
- :map(function(stat) return stat.type == 'directory' end)
- :catch(function() return false end)
- :map(function(exists)
- if exists then return end
-
- return async.task.new(function(resolve, reject)
- vim.uv.fs_mkdir(path, 511, function(err)
- if err then return reject(err) end
- resolve()
- end)
- end)
- end)
-end
-
-function files.rename(old_path, new_path)
- return async.task.new(function(resolve, reject)
- vim.uv.fs_rename(old_path, new_path, function(err)
- if err then return reject(err) end
- resolve()
- end)
- end)
-end
-
-return files
diff --git a/lua/blink/download/init.lua b/lua/blink/download/init.lua
deleted file mode 100644
index 19f3a4b..0000000
--- a/lua/blink/download/init.lua
+++ /dev/null
@@ -1,54 +0,0 @@
-local async = require('blink.download.lib.async')
-local git = require('blink.download.git')
-
---- @class blink.download.Options
---- @field download_url (fun(version: string, system_triple: string, extension: string): string) | nil
---- @field on_download fun()
---- @field root_dir string
---- @field output_dir string
---- @field binary_name string
---- @field force_version string | nil
-
---- @class blink.download.API
-local download = {}
-
---- @param options blink.download.Options
---- @param callback fun(err: string | nil)
-function download.ensure_downloaded(options, callback)
- callback = vim.schedule_wrap(callback)
-
- local files = require('blink.download.files').new(options.root_dir, options.output_dir, options.binary_name)
- require('blink.download.cpath')(files.lib_folder)
-
- async.task
- .await_all({ git.get_version(files.root_dir), files:get_version() })
- :map(function(results) return { git = results[1], current = results[2] } end)
- :map(function(version)
- -- no version file found, user manually placed the .so file or build the plugin manually
- if version.current.missing then
- local shared_library_found, _ = pcall(require, options.binary_name)
- if shared_library_found then return end
- end
-
- -- downloading disabled, not built locally
- if not options.download_url then error('No rust library found, but downloading is disabled.') end
-
- -- downloading enabled, not on a git tag
- local target_git_tag = options.force_version or version.git.tag
- if target_git_tag == nil then
- error("No rust library found, but can't download due to not being on a git tag.")
- end
-
- -- already downloaded and the correct version
- if version.current.tag == target_git_tag then return end
-
- -- download
- if options.on_download then vim.schedule(function() options.on_download() end) end
- local downloader = require('blink.download.downloader')
- return downloader.download(files, options.download_url, target_git_tag)
- end)
- :map(function() callback() end)
- :catch(function(err) callback(err) end)
-end
-
-return download
diff --git a/lua/blink/lib/build/init.lua b/lua/blink/lib/build/init.lua
new file mode 100644
index 0000000..9d79170
--- /dev/null
+++ b/lua/blink/lib/build/init.lua
@@ -0,0 +1,116 @@
+local async = require('blink.cmp.lib.async')
+local utils = require('blink.cmp.lib.utils')
+local log_file = require('blink.cmp.fuzzy.build.log')
+
+--- @class blink.lib.build
+local build = {}
+
+--- Gets the path to the blink.cmp root directory (parent of lua/)
+--- @return string
+local function get_project_root()
+ local current_file = debug.getinfo(1, 'S').source:sub(2)
+ -- Go up from lua/blink.cmp/fuzzy/build/init.lua to the project root
+ return vim.fn.fnamemodify(current_file, ':p:h:h:h:h:h:h')
+end
+
+--- @param cmd string[]
+--- @return blink.cmp.Task
+local async_system = function(cmd, opts)
+ return async.task.new(function(resolve, reject)
+ local proc = vim.system(
+ cmd,
+ vim.tbl_extend('force', {
+ cwd = get_project_root(),
+ text = true,
+ }, opts or {}),
+ vim.schedule_wrap(function(out)
+ if out.code == 0 then
+ resolve(out)
+ else
+ reject(out)
+ end
+ end)
+ )
+
+ return function() return proc:kill('TERM') end
+ end)
+end
+
+--- Detects if cargo supports +nightly
+--- @return blink.cmp.Task
+local function supports_rustup()
+ return async_system({ 'cargo', '+nightly', '--version' })
+ :map(function() return true end)
+ :catch(function() return false end)
+end
+
+--- Detect if cargo supports nightly
+--- (defaulted to nightly in rustup or globally installed without rustup)
+--- @return blink.cmp.Task
+local function supports_nightly()
+ return async_system({ 'cargo', '--version' })
+ :map(function(out) return out.stdout:match('nightly') end)
+ :catch(function() return false end)
+end
+
+local function get_cargo_cmd()
+ return async.task.all({ supports_rustup(), supports_nightly() }):map(function(results)
+ local rustup = results[1]
+ local nightly = results[2]
+
+ if rustup then return { 'cargo', '+nightly', 'build', '--release' } end
+ if nightly then return { 'cargo', 'build', '--release' } end
+
+ utils.notify({
+ { 'Rust ' },
+ { 'nightly', 'DiagnosticInfo' },
+ { ' not available via ' },
+ { 'cargo --version', 'DiagnosticInfo' },
+ { ' and rustup not detected via ' },
+ { 'cargo +nightly --version', 'DiagnosticInfo' },
+ { '. Cannot build fuzzy matching library' },
+ }, vim.log.levels.ERROR)
+ end)
+end
+
+--- Builds the rust binary from source
+--- @return blink.cmp.Task
+function build.build()
+ utils.notify({ { 'Building fuzzy matching library from source...' } }, vim.log.levels.INFO)
+
+ local log = log_file.create()
+ log.write('Working Directory: ' .. get_project_root())
+
+ return get_cargo_cmd()
+ --- @param cmd string[]
+ :map(function(cmd)
+ log.write('Command: ' .. table.concat(cmd, ' ') .. '\n')
+ log.write('\n\n---\n\n')
+
+ return async_system(cmd, {
+ stdout = function(_, data) log.write(data or '') end,
+ stderr = function(_, data) log.write(data or '') end,
+ })
+ end)
+ :map(
+ function()
+ utils.notify({
+ { 'Successfully built fuzzy matching library. ' },
+ { ':BlinkCmp build-log', 'DiagnosticInfo' },
+ }, vim.log.levels.INFO)
+ end
+ )
+ :catch(
+ function()
+ utils.notify({
+ { 'Failed to build fuzzy matching library! ', 'DiagnosticError' },
+ { ':BlinkCmp build-log', 'DiagnosticInfo' },
+ }, vim.log.levels.ERROR)
+ end
+ )
+ :map(function() log.close() end)
+end
+
+function build.build_log() log_file.open() end
+
+return build
diff --git a/lua/blink/lib/build/log.lua b/lua/blink/lib/build/log.lua
new file mode 100644
index 0000000..003a8a8
--- /dev/null
+++ b/lua/blink/lib/build/log.lua
@@ -0,0 +1,27 @@
+local log = {
+ latest_log_path = nil,
+}
+
+--- @return { path: string, write: fun(content: string), close: fun() }
+function log.create()
+ local path = vim.fn.tempname() .. '_blink_cmp_build.log'
+ log.latest_log_path = path
+
+ local file = io.open(path, 'w')
+ if not file then error('Failed to open build log file at: ' .. path) end
+ return {
+ path = path,
+ write = function(content) file:write(content) end,
+ close = function() file:close() end,
+ }
+end
+
+function log.open()
+ if log.latest_log_path == nil then
+ require('blink.cmp.lib.utils').notify({ { 'No build log available' } }, vim.log.levels.ERROR)
+ else
+ vim.cmd('edit ' .. log.latest_log_path)
+ end
+end
+
+return log
diff --git a/lua/blink/lib/config/init.lua b/lua/blink/lib/config/init.lua
new file mode 100644
index 0000000..a746db2
--- /dev/null
+++ b/lua/blink/lib/config/init.lua
@@ -0,0 +1,57 @@
+--- @class blink.lib.Filter
+--- @field bufnr? number
+
+--- @class blink.lib.Enable
+--- @field enable fun(enable: boolean, filter?: blink.lib.Filter) Enables or disables the module, optionally scoped to a buffer
+--- @field is_enabled fun(filter?: blink.lib.Filter): boolean Returns whether the module is enabled, optionally scoped to a buffer
+
+--- @class blink.lib.EnableOpts
+--- @field callback? fun(enable: boolean, filter?: blink.lib.Filter) Note that `filter.bufnr = 0` will be replaced with the current buffer
+
+--- @class blink.lib.config
+local M = {}
+
+--- @param module_name string
+--- @param opts blink.lib.EnableOpts?
+function M.new_enable(module_name, opts)
+ return {
+ enable = function(enable, filter)
+ if enable == nil then enable = true end
+
+ if filter ~= nil and filter.bufnr ~= nil then
+ if filter.bufnr == 0 then filter = { bufnr = vim.api.nvim_get_current_buf() } end
+ vim.b[filter.bufnr][module_name] = enable
+ else
+ vim.g[module_name] = enable
+ end
+
+ if opts ~= nil and opts.callback ~= nil then opts.callback(enable, filter) end
+ end,
+ is_enabled = function(filter)
+ if filter ~= nil and filter.bufnr ~= nil then
+ local bufnr = filter.bufnr == 0 and vim.api.nvim_get_current_buf() or filter.bufnr
+ if vim.b[bufnr][module_name] ~= nil then return vim.b[bufnr][module_name] == true end
+
+ -- TODO:
+ -- local blocked = config.blocked
+ -- if
+ -- (blocked.buftypes.include_defaults and vim.tbl_contains(default_blocked_buftypes, vim.bo[bufnr].buftype))
+ -- or (#blocked.buftypes > 0 and vim.tbl_contains(blocked.buftypes, vim.bo[bufnr].buftype))
+ -- or (blocked.filetypes.include_defaults and vim.tbl_contains(default_blocked_filetypes, vim.bo[bufnr].filetype))
+ -- or (#blocked.filetypes > 0 and vim.tbl_contains(blocked.filetypes, vim.bo[bufnr].filetype))
+ -- then
+ -- return false
+ -- end
+ end
+ return vim.g[module_name] ~= false
+ end,
+ }
+end
+
+--- @param schema blink.lib.ConfigSchema
+--- @param validate_defaults boolean? Validate the default values, defaults to true
+function M.new_config(schema, validate_defaults)
+ return require('blink.lib.config.schema').new(schema, validate_defaults)
+end
+
+return M
diff --git a/lua/blink/lib/config/schema.lua b/lua/blink/lib/config/schema.lua
new file mode 100644
index 0000000..be52df9
--- /dev/null
+++ b/lua/blink/lib/config/schema.lua
@@ -0,0 +1,123 @@
+-- TODO: support nested schemas
+
+local utils = require('blink.lib.config.utils')
+
+--- @class blink.lib.ConfigSchemaField
+--- @field [1] any Default value
+--- @field [2] blink.lib.ConfigSchemaType | blink.lib.ConfigSchemaType[] Allowed type or types
+--- @field [3]? fun(val): boolean | string | nil Validation function returning a string error message or false to use the default error message. Any other return value will be treated as passing validation
+--- @field [4]? string Error message to use if the validation function returns false
+
+--- @alias blink.lib.ConfigSchemaType 'string' | 'number' | 'boolean' | 'function' | 'table' | 'nil' | 'any'
+--- @alias blink.lib.ConfigSchema { [string]: blink.lib.ConfigSchema | blink.lib.ConfigSchemaField }
+
+local M = {}
+
+--- @param global_key string Key used for getting configs from `vim.g` and `vim.b`
+--- @param schema blink.lib.ConfigSchema
+--- @param validate_defaults boolean? Validate the default values, defaults to true
+function M.new(global_key, schema, validate_defaults)
+ local config = M.extract_default(schema)
+ if validate_defaults ~= false then M.validate(schema, config) end
+
+ --- @param path string[]
+ local function get_metatable(inner_schema, path)
+ local metatables = {}
+ for key, field in pairs(inner_schema) do
+ local nested_path = vim.list_extend({}, path)
+ table.insert(nested_path, key)
+ if field[2] ~= nil then metatables[key] = get_metatable(inner_schema[key], nested_path) end
+ end
+
+ return setmetatable({}, {
+ __index = function(_, key)
+ if metatables[key] ~= nil then return metatables[key] end
+
+ local buffer_value = utils.tbl_get(vim.b[global_key], path)
+ if buffer_value ~= nil then return buffer_value end
+
+ local global_value = utils.tbl_get(vim.g[global_key], path)
+ if global_value ~= nil then return global_value end
+
+ return utils.tbl_get(config, path)
+ end,
+
+ __newindex = function(_, key, value)
+ if inner_schema[key] ~= nil then
+ M.validate({ [key] = inner_schema[key] }, { [key] = value }, table.concat(path, '.') .. '.')
+ end
+ config[key] = value
+ end,
+
+ __call = function(_, tbl)
+ if #path > 0 then error('Cannot call a nested config schema') end
+
+ local new_config = vim.tbl_deep_extend('force', config, tbl or {})
+ M.validate(schema, new_config)
+ config = new_config
+ end,
+ })
+ end
+
+ return get_metatable(schema, {})
+end
+
+--- Extracts the default values from a schema
+--- @param schema blink.lib.ConfigSchema
+--- @return table
+function M.extract_default(schema)
+ local default = {}
+ for key, field in pairs(schema) do
+ if field[1] ~= nil then
+ default[key] = field[1]
+ else
+ default[key] = M.extract_default(field)
+ end
+ end
+ return default
+end
+
+--- @param schema blink.lib.ConfigSchema
+--- @param tbl table
+--- @param prev_keys string? For internal use only
+function M.validate(schema, tbl, prev_keys)
+ prev_keys = prev_keys or ''
+
+ for key, field in pairs(schema) do
+ -- nested schema
+ if field[2] == nil then
+ local nested_tbl = tbl[key]
+ if nested_tbl == nil then
+ error(string.format('Missing field %s: expected %s, got nil', prev_keys .. key, utils.format_types(field[2])))
+ end
+ M.validate(field, tbl[key], prev_keys .. key .. '.')
+
+ -- field schema
+ else
+ if not utils.validate_type(field[2], tbl[key]) then
+ error(
+ string.format(
+ "Invalid type for %s: expected %s, got '%s'",
+ prev_keys .. key,
+ utils.format_types(field[2]),
+ type(tbl[key])
+ )
+ )
+ end
+ if field[3] then
+ local err = field[3](tbl[key])
+ if err == false then
+ error(
+ string.format(
+ 'Invalid value for %s: %s',
+ prev_keys .. key,
+ field[4] or '[[developer forgot to set a default error message!]]'
+ )
+ )
+ elseif type(err) == 'string' then
+ error(string.format('Invalid value for %s: %s', prev_keys .. key, err))
+ end
+ end
+ end
+ end
+end
diff --git a/lua/blink/lib/config/utils.lua b/lua/blink/lib/config/utils.lua
new file mode 100644
index 0000000..e404cb4
--- /dev/null
+++ b/lua/blink/lib/config/utils.lua
@@ -0,0 +1,36 @@
+local utils = {}
+
+function utils.tbl_get(tbl, path)
+ for key in ipairs(path) do
+ if tbl == nil then return end
+ tbl = tbl[key]
+ end
+ return tbl
+end
+
+--- @param types blink.lib.ConfigSchemaType | blink.lib.ConfigSchemaType[]
+--- @param value any
+function utils.validate_type(types, value)
+ if type(types) ~= 'table' then return type(value) == types end
+
+ local value_type = type(value)
+ for _, type in ipairs(types) do
+ if value_type == type then return true end
+ end
+ return false
+end
+
+--- Formats a list of types into a string like "one of 'string', 'number'" or for a single type "'string'"
+--- @param types blink.lib.ConfigSchemaType | blink.lib.ConfigSchemaType[]
+function utils.format_types(types)
+ if type(types) == 'table' then
+ local str = 'one of '
+ for _, type in ipairs(types) do
+ str = str .. "'" .. type .. "', "
+ end
+ return str:sub(1, -3)
+ end
+ return "'" .. types .. "'"
+end
+
+return utils
diff --git a/lua/blink/download/config.lua b/lua/blink/lib/download/config.lua
similarity index 54%
rename from lua/blink/download/config.lua
rename to lua/blink/lib/download/config.lua
index 6d4e080..35f4248 100644
--- a/lua/blink/download/config.lua
+++ b/lua/blink/lib/download/config.lua
@@ -1,8 +1,5 @@
return {
force_system_triple = nil,
- proxy = {
- url = nil,
- from_env = true,
- },
+ proxy = { url = nil, from_env = true },
extra_curl_args = {},
}
diff --git a/lua/blink/download/cpath.lua b/lua/blink/lib/download/cpath.lua
similarity index 92%
rename from lua/blink/download/cpath.lua
rename to lua/blink/lib/download/cpath.lua
index 2714343..2bfbc2e 100644
--- a/lua/blink/download/cpath.lua
+++ b/lua/blink/lib/download/cpath.lua
@@ -1,4 +1,4 @@
-local files = require('blink.download.files')
+local files = require('blink.lib.download.files')
--- @type table
local cpath_set_by_module = {}
diff --git a/lua/blink/download/downloader.lua b/lua/blink/lib/download/downloader.lua
similarity index 85%
rename from lua/blink/download/downloader.lua
rename to lua/blink/lib/download/downloader.lua
index cb8e4d6..63bbe12 100644
--- a/lua/blink/download/downloader.lua
+++ b/lua/blink/lib/download/downloader.lua
@@ -1,13 +1,14 @@
-local async = require('blink.download.lib.async')
-local config = require('blink.download.config')
-local system = require('blink.download.system')
+local task = require('blink.lib.task')
+local config = require('blink.lib.download.config')
+local system = require('blink.lib.download.system')
+local fs = require('blink.lib.fs')
local downloader = {}
---- @param files blink.download.Files
+--- @param files blink.lib.download.files
--- @param get_download_url fun(version: string, system_triple: string, extension: string): string
--- @param version string
---- @return blink.download.Task
+--- @return blink.lib.Task
function downloader.download(files, get_download_url, version)
-- set the version to 'v0.0.0' to avoid a failure causing the pre-built binary being marked as locally built
return files
@@ -26,7 +27,7 @@ function downloader.download(files, get_download_url, version)
)
:map(
function()
- return files.rename(
+ return fs.rename(
files.lib_folder .. '/' .. files.lib_filename .. '.tmp',
files.lib_folder .. '/' .. files.lib_filename
)
@@ -35,12 +36,12 @@ function downloader.download(files, get_download_url, version)
:map(function() return files:set_version(version) end)
end
---- @param files blink.download.Files
+--- @param files blink.lib.download.files
--- @param url string
--- @param filename string
---- @return blink.download.Task
+--- @return blink.lib.Task
function downloader.download_file(files, url, filename)
- return async.task.new(function(resolve, reject)
+ return task.new(function(resolve, reject)
local args = { 'curl' }
-- Use https proxy if available
diff --git a/lua/blink/lib/download/files.lua b/lua/blink/lib/download/files.lua
new file mode 100644
index 0000000..4b554e6
--- /dev/null
+++ b/lua/blink/lib/download/files.lua
@@ -0,0 +1,58 @@
+local fs = require('blink.lib.fs')
+
+--- @class blink.lib.download.files
+--- @field root_dir string
+--- @field lib_folder string
+--- @field lib_filename string
+--- @field lib_path string
+--- @field version_path string
+local M = {}
+
+--- @param root_dir string
+--- @param output_dir string
+--- @param binary_name string
+--- @return blink.lib.download.files
+function M.new(root_dir, output_dir, binary_name)
+ -- Normalize trailing and leading slashes
+ if root_dir:sub(#root_dir, #root_dir) ~= '/' then root_dir = root_dir .. '/' end
+ if output_dir:sub(1, 1) == '/' then output_dir = output_dir:sub(2) end
+
+ local lib_folder = root_dir .. output_dir
+ local lib_filename = 'lib' .. binary_name .. M.get_lib_extension()
+ local lib_path = lib_folder .. '/' .. lib_filename
+
+ local self = setmetatable({}, { __index = M })
+
+ self.root_dir = root_dir
+ self.lib_folder = lib_folder
+ self.lib_filename = lib_filename
+ self.lib_path = lib_path
+ self.version_path = lib_folder .. '/version'
+
+ return self
+end
+
+--- @return blink.lib.Task<{ version?: string; missing?: boolean }>
+function M:get_version()
+ return fs.read(self.version_path, 1024)
+ :map(function(version) return { version = version } end)
+ :catch(function() return { missing = true } end)
+end
+
+--- @param version string
+--- @return blink.lib.Task
+function M:set_version(version)
+ return fs.mkdir(self.root_dir .. '/target')
+ :map(function() return fs.mkdir(self.lib_folder) end)
+ :map(function() return fs.write(self.version_path, version) end)
+end
+
+--- Get the extension for the library based on the current platform, including the dot (i.e. '.so' or '.dll')
+--- @return string
+function M.get_lib_extension()
+ if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then return '.dylib' end
+ if jit.os:lower() == 'windows' then return '.dll' end
+ return '.so'
+end
+
+return M
diff --git a/lua/blink/download/git.lua b/lua/blink/lib/download/git.lua
similarity index 78%
rename from lua/blink/download/git.lua
rename to lua/blink/lib/download/git.lua
index fd1006b..989aaf6 100644
--- a/lua/blink/download/git.lua
+++ b/lua/blink/lib/download/git.lua
@@ -1,12 +1,12 @@
-local async = require('blink.download.lib.async')
+local task = require('blink.lib.task')
---- @class blink.download.Git
+--- @class blink.lib.download.git
local git = {}
--- @param root_dir string
---- @return blink.download.Task
+--- @return blink.lib.Task
function git.get_version(root_dir)
- return async.task.new(function(resolve, reject)
+ return task.new(function(resolve, reject)
vim.system({ 'git', 'describe', '--tags', '--exact-match' }, { cwd = root_dir }, function(out)
if out.code == 128 then return resolve({}) end
if out.code ~= 0 then
diff --git a/lua/blink/lib/download/init.lua b/lua/blink/lib/download/init.lua
new file mode 100644
index 0000000..2b295bd
--- /dev/null
+++ b/lua/blink/lib/download/init.lua
@@ -0,0 +1,54 @@
+local task = require('blink.lib.task')
+
+--- @class blink.lib.download.Opts
+--- @field download_url? fun(version: string, system_triple: string, extension: string): string
+--- @field on_download fun()
+--- @field root_dir string
+--- @field output_dir string
+--- @field binary_name string
+--- @field force_version? string
+
+--- @class blink.lib.download
+local download = {}
+
+--- @param opts blink.lib.download.Opts
+--- @param callback fun(err?: string)
+function download.ensure_downloaded(opts, callback)
+ callback = vim.schedule_wrap(callback)
+
+ local git = require('blink.lib.download.git')
+ local files = require('blink.lib.download.files').new(opts.root_dir, opts.output_dir, opts.binary_name)
+ require('blink.lib.download.cpath')(files.lib_folder)
+
+ task
+ .all({ git.get_version(files.root_dir), files:get_version() })
+ :map(function(results) return { git = results[1], current = results[2] } end)
+ :map(function(version)
+ -- no version file found, user manually placed the .so file or build the plugin manually
+ if version.current.missing then
+ local shared_library_found, _ = pcall(require, opts.binary_name)
+ if shared_library_found then return end
+ end
+
+ -- downloading disabled, not built locally
+ if not opts.download_url then error('No rust library found, but downloading is disabled.') end
+
+ -- downloading enabled, not on a git tag
+ local target_git_tag = opts.force_version or version.git.tag
+ if target_git_tag == nil then
+ error("No rust library found, but can't download due to not being on a git tag.")
+ end
+
+ -- already downloaded and the correct version
+ if version.current.tag == target_git_tag then return end
+
+ -- download
+ if opts.on_download then vim.schedule(function() opts.on_download() end) end
+ local downloader = require('blink.lib.download.downloader')
+ return downloader.download(files, opts.download_url, target_git_tag)
+ end)
+ :map(function() callback() end)
+ :catch(function(err) callback(err) end)
+end
+
+return download
diff --git a/lua/blink/download/system.lua b/lua/blink/lib/download/system.lua
similarity index 85%
rename from lua/blink/download/system.lua
rename to lua/blink/lib/download/system.lua
index 218058b..30b381c 100644
--- a/lua/blink/download/system.lua
+++ b/lua/blink/lib/download/system.lua
@@ -1,15 +1,10 @@
-local config = require('blink.download.config')
-local async = require('blink.download.lib.async')
+local config = require('blink.lib.download.config')
+local task = require('blink.lib.task')
local system = {
triples = {
- mac = {
- arm = 'aarch64-apple-darwin',
- x64 = 'x86_64-apple-darwin',
- },
- windows = {
- x64 = 'x86_64-pc-windows-msvc',
- },
+ mac = { arm = 'aarch64-apple-darwin', x64 = 'x86_64-apple-darwin' },
+ windows = { x64 = 'x86_64-pc-windows-msvc' },
linux = {
android = 'aarch64-linux-android',
arm = function(libc) return 'aarch64-unknown-linux-' .. libc end,
@@ -29,10 +24,9 @@ end
--- Gets the system target triple from `cc -dumpmachine`
--- I.e. 'gnu' | 'musl'
---- @return blink.download.Task
+--- @return blink.lib.Task<'gnu' | 'musl'>
function system.get_linux_libc()
- return async
- .task
+ return task
-- Check for system libc via `cc -dumpmachine` by default
-- NOTE: adds 1ms to startup time
.new(function(resolve) vim.system({ 'cc', '-dumpmachine' }, { text = true }, resolve) end)
@@ -50,7 +44,7 @@ function system.get_linux_libc()
:map(function(libc)
if libc ~= nil then return libc end
- return async.task.new(function(resolve)
+ return task.new(function(resolve)
vim.uv.fs_stat('/etc/alpine-release', function(err, is_alpine)
if err then return resolve('gnu') end
resolve(is_alpine ~= nil and 'musl' or 'gnu')
@@ -60,6 +54,7 @@ function system.get_linux_libc()
end
--- Same as `system.get_linux_libc` but synchronous
+--- @return 'gnu' | 'musl'
function system.get_linux_libc_sync()
local _, process = pcall(function() return vim.system({ 'cc', '-dumpmachine' }, { text = true }):wait() end)
if process and process.code == 0 then
@@ -75,10 +70,10 @@ function system.get_linux_libc_sync()
end
--- Gets the system triple for the current system
---- I.e. `x86_64-unknown-linux-gnu` or `aarch64-apple-darwin`
---- @return blink.download.Task
+--- for example, `x86_64-unknown-linux-gnu` or `aarch64-apple-darwin`
+--- @return blink.lib.Task
function system.get_triple()
- return async.task.new(function(resolve, reject)
+ return task.new(function(resolve, reject)
if config.force_system_triple then return resolve(config.force_system_triple) end
local os, arch = system.get_info()
@@ -99,7 +94,7 @@ end
--- Same as `system.get_triple` but synchronous
--- @see system.get_triple
---- @return string | function | nil
+--- @return string?
function system.get_triple_sync()
if config.force_system_triple then return config.force_system_triple end
diff --git a/lua/blink/lib/fs.lua b/lua/blink/lib/fs.lua
new file mode 100644
index 0000000..f8c1f51
--- /dev/null
+++ b/lua/blink/lib/fs.lua
@@ -0,0 +1,139 @@
+local task = require('blink.lib.task')
+local uv = vim.uv
+
+--- @class blink.lib.fs
+local fs = {}
+
+--- @param path string
+--- @param flags string
+--- @param mode integer
+--- @return blink.lib.Task
+function fs.open(path, flags, mode)
+ return task.new(function(resolve, reject)
+ uv.fs_open(path, flags, mode, function(err, fd)
+ if err or fd == nil then return reject(err or 'Unknown error while opening file') end
+ resolve(fd)
+ end)
+ end)
+end
+
+--- Scans a directory asynchronously in chunks, calling a provided callback for each directory entry.
+--- The task resolves once all entries have been processed.
+--- @param path string
+--- @param callback fun(entries: table[]) Callback function called with an array (chunk) of directory entries
+--- @return blink.lib.Task
+function fs.list(path, callback)
+ local chunk_size = 200
+
+ return task.new(function(resolve, reject)
+ uv.fs_scandir(path, function(err, req)
+ if err or not req then return reject(err) end
+ local entries = {}
+ local function send_chunk()
+ if #entries > 0 then
+ vim.schedule_wrap(callback)(entries)
+ entries = {}
+ end
+ end
+ while true do
+ local name, type = uv.fs_scandir_next(req)
+ if not name then break end
+ table.insert(entries, { name = name, type = type })
+ if #entries >= chunk_size then send_chunk() end
+ end
+ send_chunk()
+ resolve(true)
+ end)
+ end)
+end
+
+--- Equivalent to `preadv(2)`. Returns a string where an empty string indicates EOF
+--- @param path string
+--- @param size number
+--- @param offset number?
+--- @return blink.lib.Task
+function fs.read(path, size, offset)
+ return task.new(function(resolve, reject)
+ vim.uv.fs_open(path, 'r', 438, function(open_err, fd)
+ if open_err or fd == nil then return reject(open_err or 'Unknown error while opening file') end
+ vim.uv.fs_read(fd, size, offset or 0, function(read_err, data)
+ vim.uv.fs_close(fd, function() end)
+ if read_err or data == nil then return reject(read_err or 'Unknown error while closing file') end
+ return resolve(data)
+ end)
+ end)
+ end)
+end
+
+--- Equivalent to `pwritev(2)`. Returns the number of bytes written
+--- @param path string
+--- @param data string
+--- @param offset number?
+--- @return blink.lib.Task
+function fs.write(path, data, offset)
+ return task.new(function(resolve, reject)
+ vim.uv.fs_open(path, 'w', 438, function(open_err, fd)
+ if open_err or fd == nil then return reject(open_err or 'Unknown error') end
+ vim.uv.fs_write(fd, data, offset or 0, function(write_err, bytes_written)
+ vim.uv.fs_close(fd, function() end)
+ if write_err then return reject(write_err) end
+ return resolve(bytes_written)
+ end)
+ end)
+ end)
+end
+
+--- @param path string
+--- @return blink.lib.Task
+function fs.exists(path)
+ return task.new(function(resolve)
+ vim.uv.fs_stat(path, function(err) resolve(not err) end)
+ end)
+end
+
+--- Equivalent to `stat(2)`
+--- @param path string
+--- @return blink.lib.Task
+function fs.stat(path)
+ return task.new(function(resolve, reject)
+ vim.uv.fs_stat(path, function(err, stat)
+ if err then return reject(err) end
+ resolve(stat)
+ end)
+ end)
+end
+
+--- Creates a directory (non-recursive), no-op if the directory already exists
+--- @param path string
+--- @param mode integer? Defaults to `511`
+--- @return blink.lib.Task
+function fs.mkdir(path, mode)
+ return fs.stat(path)
+ :map(function(stat) return stat.type == 'directory' end)
+ :catch(function() return false end)
+ :map(function(exists)
+ if exists then return end
+
+ return task.new(function(resolve, reject)
+ vim.uv.fs_mkdir(path, mode or 511, function(err)
+ if err then return reject(err) end
+ resolve()
+ end)
+ end)
+ end)
+end
+
+--- Equivalent to `rename(2)`
+--- @param old_path string
+--- @param new_path string
+--- @return blink.lib.Task
+function fs.rename(old_path, new_path)
+ return task.new(function(resolve, reject)
+ vim.uv.fs_rename(old_path, new_path, function(err)
+ if err then return reject(err) end
+ resolve()
+ end)
+ end)
+end
+
+return fs
diff --git a/lua/blink/lib/init.lua b/lua/blink/lib/init.lua
new file mode 100644
index 0000000..285471e
--- /dev/null
+++ b/lua/blink/lib/init.lua
@@ -0,0 +1,28 @@
+local function lazy_require(module_name)
+ local module
+ return setmetatable({}, {
+ __index = function(_, key)
+ if module == nil then module = require(module_name) end
+ return module[key]
+ end,
+ __newindex = function(_, key, value)
+ if module == nil then module = require(module_name) end
+ module[key] = value
+ end,
+ })
+end
+
+return {
+ --- @type blink.lib.build
+ build = lazy_require('blink.lib.build'),
+ --- @type blink.lib.config
+ config = lazy_require('blink.lib.config'),
+ --- @type blink.lib.download
+ download = lazy_require('blink.lib.download'),
+ --- @type blink.lib.fs
+ fs = lazy_require('blink.lib.fs'),
+ --- @type blink.lib.log
+ log = lazy_require('blink.lib.log'),
+ --- @type blink.lib.Task
+ task = lazy_require('blink.lib.task'),
+}
diff --git a/lua/blink/lib/log.lua b/lua/blink/lib/log.lua
new file mode 100644
index 0000000..7741b5d
--- /dev/null
+++ b/lua/blink/lib/log.lua
@@ -0,0 +1,83 @@
+--- @class blink.lib.Logger
+--- @field set_min_level fun(level: number)
+--- @field open fun()
+--- @field log fun(level: number, msg: string, ...: any)
+--- @field trace fun(msg: string, ...: any)
+--- @field debug fun(msg: string, ...: any)
+--- @field info fun(msg: string, ...: any)
+--- @field warn fun(msg: string, ...: any)
+--- @field error fun(msg: string, ...: any)
+
+local levels_to_str = {
+ [vim.log.levels.TRACE] = 'TRACE',
+ [vim.log.levels.DEBUG] = 'DEBUG',
+ [vim.log.levels.INFO] = 'INFO',
+ [vim.log.levels.WARN] = 'WARN',
+ [vim.log.levels.ERROR] = 'ERROR',
+}
+
+--- @class blink.lib.log
+local M = {}
+
+--- @param module_name string
+--- @param min_log_level? number
+--- @return blink.lib.Logger
+function M.new(module_name, min_log_level)
+ min_log_level = min_log_level or vim.log.levels.INFO
+
+ local queued_lines = {}
+ local path = vim.fn.stdpath('log') .. '/' .. module_name .. '.log'
+ local fd
+
+ vim.uv.fs_open(path, 'a', 438, function(err, _fd)
+ if err or _fd == nil then
+ fd = nil
+ vim.notify(
+ 'Failed to open log file at ' .. path .. ' for module ' .. module_name .. ': ' .. (err or 'Unknown error'),
+ vim.log.levels.ERROR
+ )
+ return
+ end
+
+ fd = _fd
+
+ for _, line in ipairs(queued_lines) do
+ local _, _, write_err_msg = vim.uv.fs_write(fd, line, 0)
+ if write_err_msg ~= nil then error('Failed to write to log file: ' .. (write_err_msg or 'Unknown error')) end
+ end
+ queued_lines = {}
+ end)
+
+ --- @param level number
+ --- @param msg string
+ --- @param ... any
+ local function log(level, msg, ...)
+ -- failed to initialize, ignore
+ if fd == false then return end
+
+ if level < min_log_level then return end
+ if #... > 0 then msg = msg:format(...) end
+
+ local line = levels_to_str[level] .. ': ' .. msg .. '\n'
+
+ if fd == nil then
+ table.insert(queued_lines, line)
+ else
+ local _, _, write_err_msg = vim.uv.fs_write(fd, line, 0)
+ if write_err_msg ~= nil then error('Failed to write to log file: ' .. (write_err_msg or 'Unknown error')) end
+ end
+ end
+
+ return {
+ set_min_level = function(level) min_log_level = level end,
+ open = function() vim.cmd('edit ' .. path) end,
+ log = log,
+ trace = function(msg, ...) log(vim.log.levels.TRACE, msg, ...) end,
+ debug = function(msg, ...) log(vim.log.levels.DEBUG, msg, ...) end,
+ info = function(msg, ...) log(vim.log.levels.INFO, msg, ...) end,
+ warn = function(msg, ...) log(vim.log.levels.WARN, msg, ...) end,
+ error = function(msg, ...) log(vim.log.levels.ERROR, msg, ...) end,
+ }
+end
+
+return M
diff --git a/lua/blink/download/lib/async.lua b/lua/blink/lib/task.lua
similarity index 56%
rename from lua/blink/download/lib/async.lua
rename to lua/blink/lib/task.lua
index aded829..79aabcc 100644
--- a/lua/blink/download/lib/async.lua
+++ b/lua/blink/lib/task.lua
@@ -1,29 +1,4 @@
---- Allows chaining of async operations without callback hell
----
---- @class blink.download.Task
---- @field status blink.download.TaskStatus
---- @field result any | nil
---- @field error any | nil
---- @field new fun(fn: fun(resolve: fun(result: any), reject: fun(err: any)): fun()?): blink.download.Task
----
---- @field cancel fun(self: blink.download.Task)
---- @field map fun(self: blink.download.Task, fn: fun(result: any): blink.download.Task | any): blink.download.Task
---- @field catch fun(self: blink.download.Task, fn: fun(err: any): blink.download.Task | any): blink.download.Task
---- @field schedule fun(self: blink.download.Task): blink.download.Task
---- @field timeout fun(self: blink.download.Task, ms: number): blink.download.Task
----
---- @field on_completion fun(self: blink.download.Task, cb: fun(result: any))
---- @field on_failure fun(self: blink.download.Task, cb: fun(err: any))
---- @field on_cancel fun(self: blink.download.Task, cb: fun())
---- @field _completion_cbs function[]
---- @field _failure_cbs function[]
---- @field _cancel_cbs function[]
---- @field _cancel? fun()
-local task = {
- __task = true,
-}
-
---- @enum blink.download.TaskStatus
+--- @enum blink.lib.TaskStatus
local STATUS = {
RUNNING = 1,
COMPLETED = 2,
@@ -31,6 +6,37 @@ local STATUS = {
CANCELLED = 4,
}
+---Allows chaining of cancellable async operations without callback hell. You may want to use lewis's async.nvim instead which will likely be adopted into the core: https://github.com/lewis6991/async.nvim
+---
+---```lua
+---local task = require('blink.lib.task')
+---
+---local some_task = task.new(function(resolve, reject)
+--- vim.uv.fs_readdir(vim.loop.cwd(), function(err, entries)
+--- if err ~= nil then return reject(err) end
+--- resolve(entries)
+--- end)
+---end)
+---
+---some_task
+--- :map(function(entries)
+--- return vim.tbl_map(function(entry) return entry.name end, entries)
+--- end)
+--- :catch(function(err) vim.print('failed to read directory: ' .. err) end)
+---```
+---
+---Note that lua language server cannot infer the type of the task from the `resolve` call.
+---
+---You may need to add the type annotation explicitly via an `@return` annotation on a function returning the task, or via the `@cast/@type` annotations on the task variable.
+--- @class blink.lib.Task: { status: blink.lib.TaskStatus, result: T, error: any | nil, _completion_cbs: fun(result: T)[], _failure_cbs: fun(err: any)[], _cancel_cbs: fun()[], _cancel: fun()?, __task: true }
+local task = {
+ __task = true,
+ STATUS = STATUS,
+}
+
+--- @generic T
+--- @param fn fun(resolve: fun(result?: T), reject: fun(err: any)): fun()?
+--- @return blink.lib.Task
function task.new(fn)
local self = setmetatable({}, { __index = task })
self.status = STATUS.RUNNING
@@ -62,6 +68,8 @@ function task.new(fn)
end
end
+ -- run task callback, if it returns a function, use it for cancellation
+
local success, cancel_fn_or_err = pcall(function() return fn(resolve, reject) end)
if not success then
@@ -73,6 +81,7 @@ function task.new(fn)
return self
end
+--- @param self blink.lib.Task
function task:cancel()
if self.status ~= STATUS.RUNNING then return end
self.status = STATUS.CANCELLED
@@ -85,6 +94,13 @@ end
--- mappings
+--- Creates a new task by applying a function to the result of the current task
+--- This only applies if the input task completed successfully.
+--- @generic T
+--- @generic U
+--- @param self blink.lib.Task<`T`>
+--- @param fn fun(result: T): blink.lib.Task<`U`> | `U` | nil
+--- @return blink.lib.Task
function task:map(fn)
local chained_task
chained_task = task.new(function(resolve, reject)
@@ -110,6 +126,12 @@ function task:map(fn)
return chained_task
end
+--- Creates a new task by applying a function to the error of the current task.
+--- This only applies if the input task errored.
+--- @generic T
+--- @generic U
+--- @param fn fun(self: blink.lib.Task, err: any): blink.lib.Task | U | nil
+--- @return blink.lib.Task
function task:catch(fn)
local chained_task
chained_task = task.new(function(resolve, reject)
@@ -135,6 +157,9 @@ function task:catch(fn)
return chained_task
end
+--- @generic T
+--- @param self blink.lib.Task
+--- @return blink.lib.Task
function task:schedule()
return self:map(function(value)
return task.new(function(resolve)
@@ -143,6 +168,10 @@ function task:schedule()
end)
end
+--- @generic T
+--- @param self blink.lib.Task
+--- @param ms number
+--- @return blink.lib.Task
function task:timeout(ms)
return task.new(function(resolve, reject)
vim.defer_fn(function() reject() end, ms)
@@ -152,6 +181,10 @@ end
--- events
+--- @generic T
+--- @param self blink.lib.Task
+--- @param cb fun(result: T)
+--- @return blink.lib.Task
function task:on_completion(cb)
if self.status == STATUS.COMPLETED then
cb(self.result)
@@ -161,6 +194,10 @@ function task:on_completion(cb)
return self
end
+--- @generic T
+--- @param self blink.lib.Task
+--- @param cb fun(err: any)
+--- @return blink.lib.Task
function task:on_failure(cb)
if self.status == STATUS.FAILED then
cb(self.error)
@@ -170,6 +207,10 @@ function task:on_failure(cb)
return self
end
+--- @generic T
+--- @param self blink.lib.Task
+--- @param cb fun()
+--- @return blink.lib.Task
function task:on_cancel(cb)
if self.status == STATUS.CANCELLED then
cb()
@@ -181,7 +222,14 @@ end
--- utils
-function task.await_all(tasks)
+--- Awaits all tasks in the given array of tasks.
+--- If any of the tasks fail, the returned task will fail.
+--- If any of the tasks are cancelled, the returned task will be cancelled.
+--- If all tasks are completed, the returned task will resolve with an array of results.
+--- @generic T
+--- @param tasks blink.lib.Task[]
+--- @return blink.lib.Task
+function task.all(tasks)
if #tasks == 0 then
return task.new(function(resolve) resolve({}) end)
end
@@ -201,17 +249,22 @@ function task.await_all(tasks)
end
for idx, task in ipairs(tasks) do
+ -- task completed, add result to results table, and resolve if all tasks are done
task:on_completion(function(result)
results[idx] = result
has_resolved[idx] = true
resolve_if_completed()
end)
+
+ -- one task failed, cancel all other tasks
task:on_failure(function(err)
reject(err)
- for _, task in ipairs(tasks) do
- task:cancel()
+ for _, other_task in ipairs(tasks) do
+ other_task:cancel()
end
end)
+
+ -- one task was cancelled, cancel all other tasks
task:on_cancel(function()
for _, sub_task in ipairs(tasks) do
sub_task:cancel()
@@ -224,21 +277,28 @@ function task.await_all(tasks)
end)
end
+ -- root task cancelled, cancel all inner tasks
return function()
- for _, task in ipairs(tasks) do
- task:cancel()
+ for _, other_task in ipairs(tasks) do
+ other_task:cancel()
end
end
end)
return all_task
end
+--- Creates a task that resolves with `nil`.
+--- @return blink.lib.Task
function task.empty()
- return task.new(function(resolve) resolve() end)
+ return task.new(function(resolve) resolve(nil) end)
end
-function task.identity(x)
- return task.new(function(resolve) resolve(x) end)
+--- Creates a task that resolves with the given value.
+--- @generic T
+--- @param val T
+--- @return blink.lib.Task
+function task.identity(val)
+ return task.new(function(resolve) resolve(val) end)
end
-return { task = task, STATUS = STATUS }
+return task