From f5c941bdf63acbfe0522aed84e95e22a02b15bf4 Mon Sep 17 00:00:00 2001
From: Liam Dyer
Date: Sun, 9 Nov 2025 17:11:49 -0500
Subject: [PATCH 1/3] feat: wip migration to blink.lib
---
README.md | 65 +++------
lua/blink/download.lua | 1 +
lua/blink/lib/build/init.lua | 115 ++++++++++++++++
lua/blink/lib/build/log.lua | 27 ++++
lua/blink/lib/config/init.lua | 55 ++++++++
lua/blink/lib/config/schema.lua | 123 +++++++++++++++++
lua/blink/lib/config/utils.lua | 36 +++++
lua/blink/{ => lib}/download/config.lua | 0
lua/blink/{ => lib}/download/cpath.lua | 2 +-
lua/blink/{ => lib}/download/downloader.lua | 12 +-
lua/blink/{ => lib}/download/files.lua | 34 ++---
lua/blink/{ => lib}/download/git.lua | 6 +-
lua/blink/{ => lib}/download/init.lua | 14 +-
lua/blink/{ => lib}/download/system.lua | 9 +-
lua/blink/lib/fs.lua | 118 ++++++++++++++++
lua/blink/lib/init.lua | 0
lua/blink/lib/log.lua | 29 ++++
.../{download/lib/async.lua => lib/task.lua} | 130 +++++++++++++-----
18 files changed, 659 insertions(+), 117 deletions(-)
create mode 100644 lua/blink/download.lua
create mode 100644 lua/blink/lib/build/init.lua
create mode 100644 lua/blink/lib/build/log.lua
create mode 100644 lua/blink/lib/config/init.lua
create mode 100644 lua/blink/lib/config/schema.lua
create mode 100644 lua/blink/lib/config/utils.lua
rename lua/blink/{ => lib}/download/config.lua (100%)
rename lua/blink/{ => lib}/download/cpath.lua (92%)
rename lua/blink/{ => lib}/download/downloader.lua (90%)
rename lua/blink/{ => lib}/download/files.lua (82%)
rename lua/blink/{ => lib}/download/git.lua (82%)
rename lua/blink/{ => lib}/download/init.lua (81%)
rename lua/blink/{ => lib}/download/system.lua (95%)
create mode 100644 lua/blink/lib/fs.lua
create mode 100644 lua/blink/lib/init.lua
create mode 100644 lua/blink/lib/log.lua
rename lua/blink/{download/lib/async.lua => lib/task.lua} (56%)
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/lib/build/init.lua b/lua/blink/lib/build/init.lua
new file mode 100644
index 0000000..22b624e
--- /dev/null
+++ b/lua/blink/lib/build/init.lua
@@ -0,0 +1,115 @@
+local async = require('blink.cmp.lib.async')
+local utils = require('blink.cmp.lib.utils')
+local log_file = require('blink.cmp.fuzzy.build.log')
+
+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..0d9ecc3
--- /dev/null
+++ b/lua/blink/lib/config/init.lua
@@ -0,0 +1,55 @@
+--- @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
+
+local M = {}
+
+--- @param module_name string
+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 100%
rename from lua/blink/download/config.lua
rename to lua/blink/lib/download/config.lua
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 90%
rename from lua/blink/download/downloader.lua
rename to lua/blink/lib/download/downloader.lua
index cb8e4d6..d214e05 100644
--- a/lua/blink/download/downloader.lua
+++ b/lua/blink/lib/download/downloader.lua
@@ -1,13 +1,13 @@
-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 downloader = {}
--- @param files blink.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
@@ -38,9 +38,9 @@ end
--- @param files blink.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/download/files.lua b/lua/blink/lib/download/files.lua
similarity index 82%
rename from lua/blink/download/files.lua
rename to lua/blink/lib/download/files.lua
index 1cde433..fcfa0d2 100644
--- a/lua/blink/download/files.lua
+++ b/lua/blink/lib/download/files.lua
@@ -1,4 +1,4 @@
-local async = require('blink.download.lib.async')
+local task = require('blink.lib.task')
--- @class blink.download.Files
--- @field root_dir string
@@ -11,17 +11,17 @@ local async = require('blink.download.lib.async')
---
--- @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_version fun(self: blink.download.Files): blink.lib.Task
+--- @field set_version fun(self: blink.download.Files, version: string): blink.lib.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
+--- @field read_file fun(path: string): blink.lib.Task
+--- @field write_file fun(path: string, data: string): blink.lib.Task
+--- @field exists fun(path: string): blink.lib.Task
+--- @field stat fun(path: string): blink.lib.Task
+--- @field create_dir fun(path: string): blink.lib.Task
+--- @field rename fun(old_path: string, new_path: string): blink.lib.Task
--- @type blink.download.Files
--- @diagnostic disable-next-line: missing-fields
@@ -59,7 +59,7 @@ function files:get_version()
end
--- @param version string
---- @return blink.download.Task
+--- @return blink.lib.Task
function files:set_version(version)
return files
.create_dir(self.root_dir .. '/target')
@@ -78,9 +78,9 @@ end
--- Filesystem helpers ---
--- @param path string
---- @return blink.download.Task
+--- @return blink.lib.Task
function files.read_file(path)
- return async.task.new(function(resolve, reject)
+ 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') end
vim.uv.fs_read(fd, 1024, 0, function(read_err, data)
@@ -93,7 +93,7 @@ function files.read_file(path)
end
function files.write_file(path, data)
- return async.task.new(function(resolve, reject)
+ 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, 0, function(write_err)
@@ -106,13 +106,13 @@ function files.write_file(path, data)
end
function files.exists(path)
- return async.task.new(function(resolve)
+ return 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)
+ return task.new(function(resolve, reject)
vim.uv.fs_stat(path, function(err, stat)
if err then return reject(err) end
resolve(stat)
@@ -128,7 +128,7 @@ function files.create_dir(path)
:map(function(exists)
if exists then return end
- return async.task.new(function(resolve, reject)
+ return task.new(function(resolve, reject)
vim.uv.fs_mkdir(path, 511, function(err)
if err then return reject(err) end
resolve()
@@ -138,7 +138,7 @@ function files.create_dir(path)
end
function files.rename(old_path, new_path)
- return async.task.new(function(resolve, reject)
+ return task.new(function(resolve, reject)
vim.uv.fs_rename(old_path, new_path, function(err)
if err then return reject(err) end
resolve()
diff --git a/lua/blink/download/git.lua b/lua/blink/lib/download/git.lua
similarity index 82%
rename from lua/blink/download/git.lua
rename to lua/blink/lib/download/git.lua
index fd1006b..7e633d7 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
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/download/init.lua b/lua/blink/lib/download/init.lua
similarity index 81%
rename from lua/blink/download/init.lua
rename to lua/blink/lib/download/init.lua
index 19f3a4b..8612c92 100644
--- a/lua/blink/download/init.lua
+++ b/lua/blink/lib/download/init.lua
@@ -1,5 +1,5 @@
-local async = require('blink.download.lib.async')
-local git = require('blink.download.git')
+local task = require('blink.lib.task')
+local git = require('blink.lib.download.git')
--- @class blink.download.Options
--- @field download_url (fun(version: string, system_triple: string, extension: string): string) | nil
@@ -17,11 +17,11 @@ local download = {}
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)
+ local files = require('blink.lib.download.files').new(options.root_dir, options.output_dir, options.binary_name)
+ require('blink.lib.download.cpath')(files.lib_folder)
- async.task
- .await_all({ git.get_version(files.root_dir), files:get_version() })
+ 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
@@ -44,7 +44,7 @@ function download.ensure_downloaded(options, callback)
-- download
if options.on_download then vim.schedule(function() options.on_download() end) end
- local downloader = require('blink.download.downloader')
+ local downloader = require('blink.lib.download.downloader')
return downloader.download(files, options.download_url, target_git_tag)
end)
:map(function() callback() end)
diff --git a/lua/blink/download/system.lua b/lua/blink/lib/download/system.lua
similarity index 95%
rename from lua/blink/download/system.lua
rename to lua/blink/lib/download/system.lua
index 218058b..91cb46f 100644
--- a/lua/blink/download/system.lua
+++ b/lua/blink/lib/download/system.lua
@@ -1,5 +1,5 @@
-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 = {
@@ -31,8 +31,7 @@ end
--- I.e. 'gnu' | 'musl'
--- @return blink.download.Task
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 +49,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')
diff --git a/lua/blink/lib/fs.lua b/lua/blink/lib/fs.lua
new file mode 100644
index 0000000..afc7fd5
--- /dev/null
+++ b/lua/blink/lib/fs.lua
@@ -0,0 +1,118 @@
+local task = require('blink.lib.task')
+local uv = vim.uv
+
+local fs = {}
+
+--- 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
+---
+--- If `offset` is >= 0, nil or omitted, the current file offset will be ignored.
+--- If `offset` is -1, the current file offset will be used and updated.
+--- @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
+---
+--- If `offset` is >= 0, nil or omitted, the current file offset will be ignored.
+--- If `offset` is -1, the current file offset will be used and updated.
+--- @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
+
+return fs
diff --git a/lua/blink/lib/init.lua b/lua/blink/lib/init.lua
new file mode 100644
index 0000000..e69de29
diff --git a/lua/blink/lib/log.lua b/lua/blink/lib/log.lua
new file mode 100644
index 0000000..f8af205
--- /dev/null
+++ b/lua/blink/lib/log.lua
@@ -0,0 +1,29 @@
+local log = {}
+
+--- @param module_name string
+--- @param min_log_level number
+function log.new(module_name, min_log_level)
+ local path = vim.fn.stdpath('log') .. '/' .. module_name .. '.log'
+
+ return setmetatable({
+ module_name = module_name,
+ min_log_level = min_log_level,
+ path = path,
+ }, { __index = log })
+end
+
+function log:set_min_level(level) self.min_log_level = level end
+
+function log:open() vim.cmd('edit ' .. self.path) end
+
+function log:log(level, msg)
+ if level < self.min_log_level then return end
+end
+
+function log:trace(msg) end
+function log:debug(msg) end
+function log:info(msg) end
+function log:warn(msg) end
+function log:error(msg) end
+
+return log
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..8d501a4 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 | nil, 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
From f6cf5d59ee26ad787ebf551dab509742cdd80d84 Mon Sep 17 00:00:00 2001
From: Stefan Boca <45266795+stefanboca@users.noreply.github.com>
Date: Sun, 9 Nov 2025 22:14:46 -0800
Subject: [PATCH 2/3] docs: add annotation for opts in config.new_enable (#6)
---
lua/blink/lib/config/init.lua | 1 +
1 file changed, 1 insertion(+)
diff --git a/lua/blink/lib/config/init.lua b/lua/blink/lib/config/init.lua
index 0d9ecc3..2726461 100644
--- a/lua/blink/lib/config/init.lua
+++ b/lua/blink/lib/config/init.lua
@@ -11,6 +11,7 @@
local M = {}
--- @param module_name string
+--- @param opts blink.lib.EnableOpts?
function M.new_enable(module_name, opts)
return {
enable = function(enable, filter)
From 4a911fa252c567fb203e7f362b872d0338461faf Mon Sep 17 00:00:00 2001
From: Liam Dyer
Date: Tue, 11 Nov 2025 18:32:01 -0500
Subject: [PATCH 3/3] feat: big for the culture
---
lua/blink/lib/build/init.lua | 1 +
lua/blink/lib/config/init.lua | 1 +
lua/blink/lib/download/config.lua | 5 +-
lua/blink/lib/download/downloader.lua | 7 +-
lua/blink/lib/download/files.lua | 135 +++++---------------------
lua/blink/lib/download/git.lua | 2 +-
lua/blink/lib/download/init.lua | 28 +++---
lua/blink/lib/download/system.lua | 20 ++--
lua/blink/lib/fs.lua | 33 +++++--
lua/blink/lib/init.lua | 28 ++++++
lua/blink/lib/log.lua | 94 ++++++++++++++----
lua/blink/lib/task.lua | 2 +-
12 files changed, 182 insertions(+), 174 deletions(-)
diff --git a/lua/blink/lib/build/init.lua b/lua/blink/lib/build/init.lua
index 22b624e..9d79170 100644
--- a/lua/blink/lib/build/init.lua
+++ b/lua/blink/lib/build/init.lua
@@ -2,6 +2,7 @@ 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/)
diff --git a/lua/blink/lib/config/init.lua b/lua/blink/lib/config/init.lua
index 2726461..a746db2 100644
--- a/lua/blink/lib/config/init.lua
+++ b/lua/blink/lib/config/init.lua
@@ -8,6 +8,7 @@
--- @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
diff --git a/lua/blink/lib/download/config.lua b/lua/blink/lib/download/config.lua
index 6d4e080..35f4248 100644
--- a/lua/blink/lib/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/lib/download/downloader.lua b/lua/blink/lib/download/downloader.lua
index d214e05..63bbe12 100644
--- a/lua/blink/lib/download/downloader.lua
+++ b/lua/blink/lib/download/downloader.lua
@@ -1,10 +1,11 @@
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.lib.Task
@@ -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,7 +36,7 @@ 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.lib.Task
diff --git a/lua/blink/lib/download/files.lua b/lua/blink/lib/download/files.lua
index fcfa0d2..4b554e6 100644
--- a/lua/blink/lib/download/files.lua
+++ b/lua/blink/lib/download/files.lua
@@ -1,149 +1,58 @@
-local task = require('blink.lib.task')
+local fs = require('blink.lib.fs')
---- @class blink.download.Files
+--- @class blink.lib.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.lib.Task
---- @field set_version fun(self: blink.download.Files, version: string): blink.lib.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.lib.Task
---- @field write_file fun(path: string, data: string): blink.lib.Task
---- @field exists fun(path: string): blink.lib.Task
---- @field stat fun(path: string): blink.lib.Task
---- @field create_dir fun(path: string): blink.lib.Task
---- @field rename fun(old_path: string, new_path: string): blink.lib.Task
+local M = {}
---- @type blink.download.Files
---- @diagnostic disable-next-line: missing-fields
-local files = {}
-
-function files.new(root_dir, output_dir, binary_name)
+--- @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 .. files.get_lib_extension()
+ local lib_filename = 'lib' .. binary_name .. M.get_lib_extension()
local lib_path = lib_folder .. '/' .. lib_filename
- local self = setmetatable({}, { __index = files })
+ 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.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)
+--- @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 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)
+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
---- Util ---
-
-function files.get_lib_extension()
+--- 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
---- Filesystem helpers ---
-
---- @param path string
---- @return blink.lib.Task
-function files.read_file(path)
- 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') 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 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 task.new(function(resolve)
- vim.uv.fs_stat(path, function(err) resolve(not err) end)
- end)
-end
-
-function files.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
-
-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 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 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
+return M
diff --git a/lua/blink/lib/download/git.lua b/lua/blink/lib/download/git.lua
index 7e633d7..989aaf6 100644
--- a/lua/blink/lib/download/git.lua
+++ b/lua/blink/lib/download/git.lua
@@ -1,6 +1,6 @@
local task = require('blink.lib.task')
---- @class blink.download.Git
+--- @class blink.lib.download.git
local git = {}
--- @param root_dir string
diff --git a/lua/blink/lib/download/init.lua b/lua/blink/lib/download/init.lua
index 8612c92..2b295bd 100644
--- a/lua/blink/lib/download/init.lua
+++ b/lua/blink/lib/download/init.lua
@@ -1,23 +1,23 @@
local task = require('blink.lib.task')
-local git = require('blink.lib.download.git')
---- @class blink.download.Options
---- @field download_url (fun(version: string, system_triple: string, extension: string): string) | nil
+--- @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 | nil
+--- @field force_version? string
---- @class blink.download.API
+--- @class blink.lib.download
local download = {}
---- @param options blink.download.Options
---- @param callback fun(err: string | nil)
-function download.ensure_downloaded(options, callback)
+--- @param opts blink.lib.download.Opts
+--- @param callback fun(err?: string)
+function download.ensure_downloaded(opts, callback)
callback = vim.schedule_wrap(callback)
- local files = require('blink.lib.download.files').new(options.root_dir, options.output_dir, options.binary_name)
+ 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
@@ -26,15 +26,15 @@ function download.ensure_downloaded(options, callback)
: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)
+ local shared_library_found, _ = pcall(require, opts.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
+ 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 = options.force_version or version.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
@@ -43,9 +43,9 @@ function download.ensure_downloaded(options, callback)
if version.current.tag == target_git_tag then return end
-- download
- if options.on_download then vim.schedule(function() options.on_download() end) end
+ if opts.on_download then vim.schedule(function() opts.on_download() end) end
local downloader = require('blink.lib.download.downloader')
- return downloader.download(files, options.download_url, target_git_tag)
+ return downloader.download(files, opts.download_url, target_git_tag)
end)
:map(function() callback() end)
:catch(function(err) callback(err) end)
diff --git a/lua/blink/lib/download/system.lua b/lua/blink/lib/download/system.lua
index 91cb46f..30b381c 100644
--- a/lua/blink/lib/download/system.lua
+++ b/lua/blink/lib/download/system.lua
@@ -3,13 +3,8 @@ 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,7 +24,7 @@ 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 task
-- Check for system libc via `cc -dumpmachine` by default
@@ -59,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
@@ -74,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()
@@ -98,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
index afc7fd5..f8c1f51 100644
--- a/lua/blink/lib/fs.lua
+++ b/lua/blink/lib/fs.lua
@@ -1,8 +1,22 @@
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
@@ -34,9 +48,6 @@ function fs.list(path, callback)
end
--- Equivalent to `preadv(2)`. Returns a string where an empty string indicates EOF
----
---- If `offset` is >= 0, nil or omitted, the current file offset will be ignored.
---- If `offset` is -1, the current file offset will be used and updated.
--- @param path string
--- @param size number
--- @param offset number?
@@ -55,9 +66,6 @@ function fs.read(path, size, offset)
end
--- Equivalent to `pwritev(2)`. Returns the number of bytes written
----
---- If `offset` is >= 0, nil or omitted, the current file offset will be ignored.
---- If `offset` is -1, the current file offset will be used and updated.
--- @param path string
--- @param data string
--- @param offset number?
@@ -115,4 +123,17 @@ function fs.mkdir(path, mode)
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
index e69de29..285471e 100644
--- a/lua/blink/lib/init.lua
+++ 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
index f8af205..7741b5d 100644
--- a/lua/blink/lib/log.lua
+++ b/lua/blink/lib/log.lua
@@ -1,29 +1,83 @@
-local log = {}
+--- @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
-function log.new(module_name, min_log_level)
+--- @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
- return setmetatable({
- module_name = module_name,
- min_log_level = min_log_level,
- path = path,
- }, { __index = log })
-end
+ 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
-function log:set_min_level(level) self.min_log_level = level end
+ fd = _fd
-function log:open() vim.cmd('edit ' .. self.path) end
+ 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)
-function log:log(level, msg)
- if level < self.min_log_level then return end
-end
+ --- @param level number
+ --- @param msg string
+ --- @param ... any
+ local function log(level, msg, ...)
+ -- failed to initialize, ignore
+ if fd == false then return end
-function log:trace(msg) end
-function log:debug(msg) end
-function log:info(msg) end
-function log:warn(msg) end
-function log:error(msg) 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 log
+return M
diff --git a/lua/blink/lib/task.lua b/lua/blink/lib/task.lua
index 8d501a4..79aabcc 100644
--- a/lua/blink/lib/task.lua
+++ b/lua/blink/lib/task.lua
@@ -28,7 +28,7 @@ local STATUS = {
---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 | nil, error: any | nil, _completion_cbs: fun(result: T)[], _failure_cbs: fun(err: any)[], _cancel_cbs: fun()[], _cancel: fun()?, __task: true }
+--- @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,