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,