diff --git a/data/plenary/filetypes/base.lua b/data/plenary/filetypes/base.lua index 1adffb3f3..c2063eed6 100644 --- a/data/plenary/filetypes/base.lua +++ b/data/plenary/filetypes/base.lua @@ -1,3 +1,4 @@ +---@type PlenaryFiletypeTable return { extension = { ['ncl'] = [[text]], diff --git a/data/plenary/filetypes/builtin.lua b/data/plenary/filetypes/builtin.lua index 6f341d609..5b954edab 100644 --- a/data/plenary/filetypes/builtin.lua +++ b/data/plenary/filetypes/builtin.lua @@ -10,6 +10,7 @@ local shebang_fts = { ['zsh'] = 'zsh', } +---@type table local shebang = {} for _, prefix in ipairs(shebang_prefixes) do for k, v in pairs(shebang_fts) do @@ -17,6 +18,7 @@ for _, prefix in ipairs(shebang_prefixes) do end end +---@type PlenaryFiletypeTable return { extension = { ['_coffee'] = 'coffee', diff --git a/lua/plenary/async/api.lua b/lua/plenary/async/api.lua index 2a4e9c0c4..6418e9052 100644 --- a/lua/plenary/async/api.lua +++ b/lua/plenary/async/api.lua @@ -1,5 +1,8 @@ local util = require "plenary.async.util" +---@alias PlenaryAsyncApi table + +---@type PlenaryAsyncApi return setmetatable({}, { __index = function(t, k) return function(...) diff --git a/lua/plenary/async/async.lua b/lua/plenary/async/async.lua index ff49288d0..3269c0e55 100644 --- a/lua/plenary/async/async.lua +++ b/lua/plenary/async/async.lua @@ -4,13 +4,20 @@ local errors = require "plenary.errors" local traceback_error = errors.traceback_error local f = require "plenary.functional" +---@class PlenaryAsyncAsync local M = {} +---@param fn any +---@return boolean local function is_callable(fn) return type(fn) == "function" or (type(fn) == "table" and type(getmetatable(fn)["__call"]) == "function") end ---because we can't store varargs +---@param step function +---@param thread thread +---@param callback? function +---@param ... any local function callback_or_next(step, thread, callback, ...) local stat = f.first(...) @@ -33,12 +40,12 @@ local function callback_or_next(step, thread, callback, ...) end ---Executes a future with a callback when it is done ----@param async_function Future: the future to execute ----@param callback function: the callback to call when done -local execute = function(async_function, callback, ...) - assert(is_callable(async_function), "type error :: expected func") +---@param func function the future to execute +---@param callback? function the callback to call when done +local execute = function(func, callback, ...) + assert(is_callable(func), "type error :: expected func") - local thread = co.create(async_function) + local thread = co.create(func) local step step = function(...) @@ -48,31 +55,41 @@ local execute = function(async_function, callback, ...) step(...) end +---A function including async logic +---@alias PlenaryAsyncFunction async fun(...): ... + local add_leaf_function do ---A table to store all leaf async functions + ---@type table _PlenaryLeafTable = setmetatable({}, { __mode = "k", }) + ---@param async_func PlenaryAsyncFunction + ---@param argc integer add_leaf_function = function(async_func, argc) assert(_PlenaryLeafTable[async_func] == nil, "Async function should not already be in the table") _PlenaryLeafTable[async_func] = argc end + ---@param async_func PlenaryAsyncFunction + ---@return boolean function M.is_leaf_function(async_func) return _PlenaryLeafTable[async_func] ~= nil end + ---@param async_func PlenaryAsyncFunction + ---@return integer function M.get_leaf_function_argc(async_func) return _PlenaryLeafTable[async_func] end end ---Creates an async function with a callback style function. ----@param func function: A callback style function to be converted. The last argument must be the callback. ----@param argc number: The number of arguments of func. Must be included. ----@return function: Returns an async function +---@param func function A callback style function to be converted. The last argument must be the callback. +---@param argc integer The number of arguments of func. Must be included. +---@return PlenaryAsyncFunction leaf Returns an leaf M.wrap = function(func, argc) if not is_callable(func) then traceback_error("type error :: expected func, got " .. type(func)) @@ -82,6 +99,7 @@ M.wrap = function(func, argc) traceback_error("type error :: expected number, got " .. type(argc)) end + ---@type PlenaryAsyncFunction local function leaf(...) local nargs = select("#", ...) @@ -99,13 +117,13 @@ end ---Use this to either run a future concurrently and then do something else ---or use it to run a future with a callback in a non async context ----@param async_function function +---@param func PlenaryAsyncFunction|function ---@param callback function -M.run = function(async_function, callback) - if M.is_leaf_function(async_function) then - async_function(callback) +M.run = function(func, callback) + if M.is_leaf_function(func) then + func(callback) else - execute(async_function, callback) + execute(func, callback) end end diff --git a/lua/plenary/async/control.lua b/lua/plenary/async/control.lua index 3a8a8ed64..9a6eb1261 100644 --- a/lua/plenary/async/control.lua +++ b/lua/plenary/async/control.lua @@ -2,22 +2,26 @@ local a = require "plenary.async.async" local Deque = require("plenary.async.structs").Deque local tbl = require "plenary.tbl" +---@class PlenaryAsyncControl local M = {} +---@class PlenaryAsyncCondvar +---@field handles (fun(): nil)[] local Condvar = {} Condvar.__index = Condvar ----@class Condvar ----@return Condvar +---@return PlenaryAsyncCondvar function Condvar.new() return setmetatable({ handles = {} }, Condvar) end ---`blocks` the thread until a notification is received +---@param self PlenaryAsyncCondvar +---@param callback fun(): nil Condvar.wait = a.wrap(function(self, callback) -- not calling the callback will block the coroutine table.insert(self.handles, callback) -end, 2) +end, 2) --[[@as async fun(): nil]] ---notify everyone that is waiting on this Condvar function Condvar:notify_all() @@ -52,12 +56,14 @@ end M.Condvar = Condvar +---@class PlenaryAsyncSemaphore +---@field handles (fun(permit: PlenaryAsyncPermit): nil)[] +---@field permits integer local Semaphore = {} Semaphore.__index = Semaphore ----@class Semaphore ----@param initial_permits number: the number of permits that it can give out ----@return Semaphore +---@param initial_permits integer the number of permits that it can give out +---@return PlenaryAsyncSemaphore function Semaphore.new(initial_permits) vim.validate { initial_permits = { @@ -79,6 +85,8 @@ end ---permit:forget() ---when a permit can be acquired returns it ---call permit:forget() to forget the permit +---@param self PlenaryAsyncSemaphore +---@param callback fun(permit: PlenaryAsyncPermit): nil Semaphore.acquire = a.wrap(function(self, callback) if self.permits > 0 then self.permits = self.permits - 1 @@ -87,8 +95,10 @@ Semaphore.acquire = a.wrap(function(self, callback) return end + ---@class PlenaryAsyncPermit local permit = {} + ---@param self_permit PlenaryAsyncPermit permit.forget = function(self_permit) self.permits = self.permits + 1 @@ -99,16 +109,17 @@ Semaphore.acquire = a.wrap(function(self, callback) end callback(permit) -end, 2) +end, 2) --[[@as async fun(self: PlenaryAsyncSemaphore): PlenaryAsyncPermit]] M.Semaphore = Semaphore +---@class PlenaryAsyncControlChannel M.channel = {} ---Creates a oneshot channel ---returns a sender and receiver function ---the sender is not async while the receiver is ----@return function, function +---@return fun(...): nil tx, async fun(): ... rx M.channel.oneshot = function() local val = nil local saved_callback = nil @@ -147,20 +158,26 @@ M.channel.oneshot = function() if is_single then return callback(val) else - return callback(tbl.unpack(val)) + return callback(tbl.unpack(val --[[@as table]])) end else saved_callback = callback end - end, 1) + end, 1) --[[@as async fun(): ...]] return sender, receiver end +---@class PlenaryAsyncCounterTx +---@field send fun(): nil + +---@class PlenaryAsyncCounterRx +---@field recv async fun(): nil +---@field last async fun(): nil + ---A counter channel. ---Basically a channel that you want to use only to notify and not to send any actual values. ----@return function: sender ----@return function: receiver +---@return PlenaryAsyncCounterTx tx, PlenaryAsyncCounterRx rx M.channel.counter = function() local counter = 0 local condvar = Condvar.new() @@ -191,9 +208,15 @@ M.channel.counter = function() return Sender, Receiver end +---@class PlenaryAsyncMpscTx +---@field send fun(...: any): nil + +---@class PlenaryAsyncMpscRx +---@field recv async fun(): ... +---@field last async fun(): ... + ---A multiple producer single consumer channel ----@return table ----@return table +---@return PlenaryAsyncMpscTx, PlenaryAsyncMpscRx M.channel.mpsc = function() local deque = Deque.new() local condvar = Condvar.new() diff --git a/lua/plenary/async/init.lua b/lua/plenary/async/init.lua index 027614ad4..dad18d3ab 100644 --- a/lua/plenary/async/init.lua +++ b/lua/plenary/async/init.lua @@ -12,6 +12,13 @@ local lookups = { control = "plenary.async.control", } +---@class PlenaryAsync: PlenaryAsyncAsync +---@field api PlenaryAsyncApi +---@field control PlenaryAsyncControl +---@field lsp PlenaryAsyncLsp +---@field tests PlenaryAsyncTests +---@field util PlenaryAsyncUtil +---@field uv PlenaryAsyncUv local exports = setmetatable(require "plenary.async.async", { __index = function(t, k) local require_path = lookups[k] diff --git a/lua/plenary/async/lsp.lua b/lua/plenary/async/lsp.lua index e76e4c4fe..f05f91c9c 100644 --- a/lua/plenary/async/lsp.lua +++ b/lua/plenary/async/lsp.lua @@ -1,12 +1,29 @@ local a = require "plenary.async.async" +---@class PlenaryAsyncLsp local M = {} +---@alias ClientRequestIds table + ---This will be deprecated because the callback can be called multiple times. ---This will give a coroutine error because the coroutine will be resumed multiple times. ---Please use buf_request_all instead. +---@type async fun(bufnr: integer, method: string, params?: table, handler?: lsp.Handler): ClientRequestIds, function M.buf_request = a.wrap(vim.lsp.buf_request, 4) +---@alias BufRequestAllHandler fun(results: table) + +---Sends an async request for all active clients attached to the buffer and executes the `handler` +---callback with the combined result. +--- +---* param bufnr (integer) Buffer handle, or 0 for current. +---* param method (string) LSP method name +---* param params (table|nil) Parameters to send to the server +---* param handler fun(results: table) (function) +--- Handler called after all requests are completed. Server results are passed as +--- a `client_id:result` map. +---* return function cancel Function that cancels all requests. +---@type async fun(bufnr: integer, method: string, params?: table, handler: BufRequestAllHandler): function M.buf_request_all = a.wrap(vim.lsp.buf_request_all, 4) return M diff --git a/lua/plenary/async/structs.lua b/lua/plenary/async/structs.lua index c133a2163..403ad4f71 100644 --- a/lua/plenary/async/structs.lua +++ b/lua/plenary/async/structs.lua @@ -1,12 +1,15 @@ +---@class PlenaryAsyncStructs local M = {} +---A double ended queue +---@class PlenaryDeque +---@field first integer +---@field last integer +---@field [integer] any local Deque = {} Deque.__index = Deque ----@class Deque ----A double ended queue ---- ----@return Deque +---@return PlenaryDeque function Deque.new() -- the indexes are created with an offset so that the indices are consequtive -- otherwise, when both pushleft and pushright are used, the indices will have a 1 length hole in the middle @@ -62,13 +65,13 @@ function Deque:is_empty() end ---returns the number of elements of the deque ----@return number +---@return integer function Deque:len() return self.last - self.first + 1 end ---returns and iterator of the indices and values starting from the left ----@return function +---@return fun(): integer?, any? function Deque:ipairs_left() local i = self.first @@ -85,7 +88,7 @@ function Deque:ipairs_left() end ---returns and iterator of the indices and values starting from the right ----@return function +---@return fun(): integer?, any? function Deque:ipairs_right() local i = self.last diff --git a/lua/plenary/async/tests.lua b/lua/plenary/async/tests.lua index d10db4871..117d3b21f 100644 --- a/lua/plenary/async/tests.lua +++ b/lua/plenary/async/tests.lua @@ -1,23 +1,31 @@ local util = require "plenary.async.util" +---@class PlenaryAsyncTests local M = {} +---@param s string +---@param async_func PlenaryAsyncFunction M.describe = function(s, async_func) describe(s, async_func) end +---@param s string +---@param async_func PlenaryAsyncFunction M.it = function(s, async_func) it(s, util.will_block(async_func, tonumber(vim.env.PLENARY_TEST_TIMEOUT))) end +---@param async_func PlenaryAsyncFunction M.pending = function(async_func) pending(async_func) end +---@param async_func PlenaryAsyncFunction M.before_each = function(async_func) before_each(util.will_block(async_func)) end +---@param async_func PlenaryAsyncFunction M.after_each = function(async_func) after_each(util.will_block(async_func)) end diff --git a/lua/plenary/async/util.lua b/lua/plenary/async/util.lua index b80f3fd10..27c8e935f 100644 --- a/lua/plenary/async/util.lua +++ b/lua/plenary/async/util.lua @@ -4,21 +4,23 @@ local vararg = require "plenary.vararg" local control = require "plenary.async.control" local channel = control.channel +---@class PlenaryAsyncUtil local M = {} +---@param timeout integer Number of milliseconds to wait before calling `fn` +---@param callback function Callback to call once `timeout` expires local defer_swapped = function(timeout, callback) vim.defer_fn(callback, timeout) end ---Sleep for milliseconds ----@param ms number -M.sleep = a.wrap(defer_swapped, 2) +M.sleep = a.wrap(defer_swapped, 2) --[[@as async fun(timeout: integer): nil]] ---This will COMPLETELY block neovim ---please just use a.run unless you have a very special usecase ---for example, in plenary test_harness you must use this ----@param async_function Future ----@param timeout number: Stop blocking if the timeout was surpassed. Default 2000. +---@param async_function PlenaryAsyncFunction +---@param timeout? integer Stop blocking if the timeout was surpassed. Default 2000. M.block_on = function(async_function, timeout) async_function = M.protected(async_function) @@ -42,14 +44,16 @@ M.block_on = function(async_function, timeout) end ---@see M.block_on ----@param async_function Future ----@param timeout number +---@param async_function PlenaryAsyncFunction +---@param timeout? integer M.will_block = function(async_function, timeout) return function() M.block_on(async_function, timeout) end end +---@param async_fns PlenaryAsyncFunction[] +---@return table M.join = function(async_fns) local len = #async_fns local results = {} @@ -81,8 +85,9 @@ M.join = function(async_fns) end ---Returns a result from the future that finishes at the first ----@param async_functions table: The futures that you want to select ----@return ... +---* param async_functions table: The futures that you want to select +---@param async_functions PlenaryAsyncFunction[] +---@param step fun(...: any): ...: any M.run_first = a.wrap(function(async_functions, step) local ran = false @@ -98,12 +103,13 @@ M.run_first = a.wrap(function(async_functions, step) async_function(callback) end -end, 2) +end, 2) --[[@as async fun(async_functions: PlenaryAsyncFunction[]): ...]] ---Returns a result from the functions that finishes at the first ----@param funcs table: The async functions that you want to select +---@param funcs function[]: The async functions that you want to select ---@return ... M.race = function(funcs) + ---@type PlenaryAsyncFunction[] local async_functions = vim.tbl_map(function(func) return function(callback) a.run(func, callback) @@ -112,27 +118,36 @@ M.race = function(funcs) return M.run_first(async_functions) end +---@param async_fns PlenaryAsyncFunction[] +---@param callback fun(...: any): ...: any M.run_all = function(async_fns, callback) a.run(function() M.join(async_fns) end, callback) end -function M.apcall(async_fn, ...) - local nargs = a.get_leaf_function_argc(async_fn) +---@async +---@param leaf PlenaryAsyncLeaf|function +---@param ... any +---@return boolean, ... +function M.apcall(leaf, ...) + local nargs = a.get_leaf_function_argc(leaf) if nargs then local tx, rx = channel.oneshot() - local stat, ret = pcall(async_fn, vararg.rotate(nargs, tx, ...)) + local stat, ret = pcall(leaf, vararg.rotate(nargs, tx, ...)) if not stat then return stat, ret else return stat, rx() end else - return pcall(async_fn, ...) + return pcall(leaf, ...) end end +---comment +---@param async_fn PlenaryAsyncFunction +---@return PlenaryAsyncFunction function M.protected(async_fn) return function() return M.apcall(async_fn) @@ -140,6 +155,6 @@ function M.protected(async_fn) end ---An async function that when called will yield to the neovim scheduler to be able to call the api. -M.scheduler = a.wrap(vim.schedule, 1) +M.scheduler = a.wrap(vim.schedule, 1) --[[@as async fun(): nil]] return M diff --git a/lua/plenary/async/uv_async.lua b/lua/plenary/async/uv_async.lua index 427e1a591..eeff24678 100644 --- a/lua/plenary/async/uv_async.lua +++ b/lua/plenary/async/uv_async.lua @@ -1,6 +1,7 @@ local a = require "plenary.async.async" local uv = vim.loop +---@class PlenaryAsyncUv local M = {} local function add(name, argc, custom) diff --git a/lua/plenary/async_lib/util.lua b/lua/plenary/async_lib/util.lua index 1de65fe4f..394d1397c 100644 --- a/lua/plenary/async_lib/util.lua +++ b/lua/plenary/async_lib/util.lua @@ -68,8 +68,6 @@ end) local Condvar = {} Condvar.__index = Condvar ----@class Condvar ----@return Condvar function Condvar.new() return setmetatable({ handles = {} }, Condvar) end @@ -108,9 +106,6 @@ M.Condvar = Condvar local Semaphore = {} Semaphore.__index = Semaphore ----@class Semaphore ----@param initial_permits number: the number of permits that it can give out ----@return Semaphore function Semaphore.new(initial_permits) vim.validate { initial_permits = { diff --git a/lua/plenary/class.lua b/lua/plenary/class.lua index e82f073c2..196970399 100644 --- a/lua/plenary/class.lua +++ b/lua/plenary/class.lua @@ -4,19 +4,19 @@ ---Copyright (c) 2014, rxi ---@brief ]] ----@class Object +---@class PlenaryClass +---@overload fun(...): PlenaryClass local Object = {} Object.__index = Object ---Does nothing. ---You have to implement this yourself for extra functionality when initializing ----@param self Object +---@type fun(self: PlenaryClass, ...) function Object:new() end ---Create a new class/object by extending the base Object class. ---The extended object will have a field called `super` that will access the super class. ----@param self Object ----@return Object +---@return PlenaryClass function Object:extend() local cls = {} for k, v in pairs(self) do @@ -31,8 +31,7 @@ function Object:extend() end ---Implement a mixin onto this Object. ----@param self Object ----@param nil ... +---@param ... any function Object:implement(...) for _, cls in pairs { ... } do for k, v in pairs(cls) do @@ -45,8 +44,7 @@ end ---Checks if the object is an instance ---This will start with the lowest class and loop over all the superclasses. ----@param self Object ----@param T Object +---@param T PlenaryClass ---@return boolean function Object:is(T) local mt = getmetatable(self) @@ -61,16 +59,14 @@ end ---The default tostring implementation for an object. ---You can override this to provide a different tostring. ----@param self Object ---@return string function Object:__tostring() return "Object" end ---You can call the class the initialize it without using `Object:new`. ----@param self Object ----@param nil ... ----@return Object +---@param ... any +---@return PlenaryClass function Object:__call(...) local obj = setmetatable({}, self) obj:new(...) diff --git a/lua/plenary/context_manager.lua b/lua/plenary/context_manager.lua index 7d617da24..7a16c2712 100644 --- a/lua/plenary/context_manager.lua +++ b/lua/plenary/context_manager.lua @@ -1,8 +1,17 @@ --- I like context managers for Python --- I want them in Lua. +---@class PlenaryContextManager local context_manager = {} +---@class PlenaryContextManagerObject +---@field enter fun(self: PlenaryContextManagerObject): any +---@field exit fun(self: PlenaryContextManagerObject) + +---@generic T, U +---@param obj function|PlenaryContextManagerObject|thread +---@param callable fun(arg: T): U? +---@return U? function context_manager.with(obj, callable) -- Wrap functions for people since we're nice if type(obj) == "function" then @@ -36,15 +45,16 @@ function context_manager.with(obj, callable) return result end end - ---- @param filename string|table -- If string, used as io.open(filename) ---- Else, should be a table with `filename` as an attribute +---If string, used as io.open(filename). Else, should be a table with `filename` as an attribute +---@param filename string|{ filename: string } +---@param mode? openmode +---@return thread function context_manager.open(filename, mode) if type(filename) == "table" and filename.filename then filename = filename.filename end - local file_io = assert(io.open(filename, mode)) + local file_io = assert(io.open(filename --[[@as string]], mode)) return coroutine.create(function() coroutine.yield(file_io) diff --git a/lua/plenary/curl.lua b/lua/plenary/curl.lua index 321bed8a5..017910707 100644 --- a/lua/plenary/curl.lua +++ b/lua/plenary/curl.lua @@ -1,3 +1,5 @@ +-- luacheck: push ignore 631 + --[[ Curl Wrapper @@ -28,6 +30,9 @@ see test/plenary/curl_spec.lua for examples. author = github.com/tami5 ]] -- +---@alias PlenaryCurlMethod fun(url: string|PlenaryCurlOptions, opts?: PlenaryCurlOptions): PlenaryCurlResponse|PlenaryJob|string[] + +-- luacheck: pop local util, parse = {}, {} @@ -41,6 +46,8 @@ local compat = require "plenary.compat" -- Utils ---------------------------------------------------- ------------------------------------------------------------- +---@param str string|integer +---@return string|integer util.url_encode = function(str) if type(str) ~= "number" then str = str:gsub("\r?\n", "\r\n") @@ -54,12 +61,20 @@ util.url_encode = function(str) end end +---@param kv table +---@param prefix string +---@param sep string +---@return string[] util.kv_to_list = function(kv, prefix, sep) return compat.flatten(F.kv_map(function(kvp) return { prefix, kvp[1] .. sep .. kvp[2] } end, kv)) end +---@param kv table +---@param sep? string +---@param kvsep string +---@return string util.kv_to_str = function(kv, sep, kvsep) return F.join( F.kv_map(function(kvp) @@ -69,6 +84,7 @@ util.kv_to_str = function(kv, sep, kvsep) ) end +---@return { [1]: string, [2]: string } util.gen_dump_path = function() local path local id = string.gsub("xxxx4xxx", "[xy]", function(l) @@ -87,15 +103,21 @@ end -- Parsers ---------------------------------------------------- --------------------------------------------------------------- +---comment +---@param t? table +---@return string[]? parse.headers = function(t) if not t then return end + ---@param str string + ---@return string local upper = function(str) return string.gsub(" " .. str, "%W%l", string.upper):sub(2) end return util.kv_to_list( (function() + ---@type table local normilzed = {} for k, v in pairs(t) do normilzed[upper(k:gsub("_", "%-"))] = v @@ -107,6 +129,8 @@ parse.headers = function(t) ) end +---@param t? table +---@return string[]? parse.data_body = function(t) if not t then return @@ -114,6 +138,8 @@ parse.data_body = function(t) return util.kv_to_list(t, "-d", "=") end +---@param xs? string|table +---@return string[]? parse.raw_body = function(xs) if not xs then return @@ -125,6 +151,8 @@ parse.raw_body = function(xs) end end +---@param t? table +---@return string[]? parse.form = function(t) if not t then return @@ -132,6 +160,8 @@ parse.form = function(t) return util.kv_to_list(t, "-F", "=") end +---@param t? table +---@return string? parse.curl_query = function(t) if not t then return @@ -139,6 +169,8 @@ parse.curl_query = function(t) return util.kv_to_str(t, "&", "=") end +---@param s? string +---@return { [1]: "-I"|"-X", [2]: string? }? parse.method = function(s) if not s then return @@ -150,6 +182,8 @@ parse.method = function(s) end end +---@param p? string +---@return { [1]: "-d", [2]: string }? parse.file = function(p) if not p then return @@ -157,6 +191,8 @@ parse.file = function(p) return { "-d", "@" .. P.expand(P.new(p)) } end +---@param xs string|table +---@return { [1]: "-u", [2]: string }? parse.auth = function(xs) if not xs then return @@ -164,18 +200,23 @@ parse.auth = function(xs) return { "-u", type(xs) == "table" and util.kv_to_str(xs, nil, ":") or xs } end +---@param xs string +---@param q table +---@return string? parse.url = function(xs, q) if not xs then return end - q = parse.curl_query(q) + local query = parse.curl_query(q) if type(xs) == "string" then - return q and xs .. "?" .. q or xs + return query and xs .. "?" .. query or xs elseif type(xs) == "table" then error "Low level URL definition is not supported." end end +---@param s string? +---@return { [1]: "-H", [2]: string }? parse.accept_header = function(s) if not s then return @@ -183,6 +224,8 @@ parse.accept_header = function(s) return { "-H", "Accept: " .. s } end +---@param s string? +---@return { [1]: string }? parse.http_version = function(s) if not s then return @@ -198,9 +241,36 @@ end -- Parse Request ------------------------------------------- ------------------------------------------------------------ +---@class PlenaryCurlOptions +---@field auth? string|table Basic request auth, 'user:pass', or {"user", "pass"} +---@field body? string|string[] The request body +---@field dry_run? boolean whether to return the args to be ran through curl. +---@field form? table request form +---@field http_version? string HTTP version to use: 'HTTP/0.9', 'HTTP/1.0', 'HTTP/1.1', 'HTTP/2', or 'HTTP/3' +---@field insecure? boolean Allow insecure server connections +---@field output? string where to download something. +---@field proxy? string [protocol://]host[:port] Use this proxy +---@field query? table url query, append after the url +---@field raw? string[] any additonal curl args, it must be an array/list. +---@field timeout? integer request timeout in mseconds +---@field url? string The url to make the request to. +---@field accept? string +---@field callback? fun(response: PlenaryCurlResponse) +---@field compressed? boolean +---@field data? string[] +---@field dump? string +---@field headers? string[] +---@field in_file? string +---@field method? string +---@field on_error? fun(err: { message: string, stderr: string, exit: integer }) +---@field raw_body? string +---@field stream? PlenaryJobCallback + +---@param opts PlenaryCurlOptions +---@return string[] result, PlenaryCurlOptions opts parse.request = function(opts) if opts.body then - local b = opts.body + local b = opts.body --[[@as string|string[] ]] local silent_is_file = function() local status, result = pcall(P.is_file, P.new(b)) return status and result @@ -249,6 +319,16 @@ end -- Parse response ------------------------------------------ ------------------------------------------------------------ +---@class PlenaryCurlResponse +---@field status integer +---@field headers string[] +---@field body string +---@field exit integer + +---@param lines string[] +---@param dump_path string +---@param code integer +---@return PlenaryCurlResponse parse.response = function(lines, dump_path, code) local headers = P.readlines(dump_path) local status = nil @@ -275,6 +355,8 @@ parse.response = function(lines, dump_path, code) } end +---@param specs PlenaryCurlOptions +---@return PlenaryCurlResponse|PlenaryJob|string[] local request = function(specs) local response = {} local args, opts = parse.request(vim.tbl_extend("force", { @@ -287,6 +369,7 @@ local request = function(specs) return args end + ---@type PlenaryJobOptions local job_opts = { command = vim.g.plenary_curl_bin_path or "curl", args = args, @@ -310,7 +393,7 @@ local request = function(specs) error(message) end end - local output = parse.response(j:result(), opts.dump[2], code) + local output = parse.response((j:result() --[[@as string[] ]]), opts.dump[2], code) if opts.callback then return opts.callback(output) else @@ -331,7 +414,21 @@ end -- Main ---------------------------------------------------- ------------------------------------------------------------ + +---@class PlenaryCurl +---@field get PlenaryCurlMethod +---@field post PlenaryCurlMethod +---@field put PlenaryCurlMethod +---@field head PlenaryCurlMethod +---@field patch PlenaryCurlMethod +---@field delete PlenaryCurlMethod +---@field request PlenaryCurlMethod + +---@return PlenaryCurl return (function() + local spec = {} + ---@param method "get"|"post"|"put"|"head"|"patch"|"delete"|"request" + ---@return PlenaryCurlMethod local partial = function(method) return function(url, opts) local spec = {} diff --git a/lua/plenary/enum.lua b/lua/plenary/enum.lua index 22d441072..d0a5b6109 100644 --- a/lua/plenary/enum.lua +++ b/lua/plenary/enum.lua @@ -25,12 +25,20 @@ --- In case of name or value clashing, the call will fail. For this reason, it's --- best if you define the members in ascending order. ---@brief ]] -local Enum = {} +---@class PlenaryEnum +---@field is_enum fun(tbl: table): boolean +---@field make_enum fun(tbl: PlenaryEnumRawTable): PlenaryEnumEnum +---@overload fun(tbl: PlenaryEnumRawTable): PlenaryEnumEnum ----@class Enum +---@alias PlenaryEnumRawTable (string|{ [1]: string, [2]: integer })[] ----@class Variant +---@class PlenaryEnumEnum +---@field [integer] string +---@field [string] PlenaryVariant +local Enum = {} +---@param name string +---@return string local function validate_member_name(name) if #name > 0 and name:sub(1, 1):match "%u" then return name @@ -46,13 +54,19 @@ end --- {'Qux', 10} --- } --- ---- @return Enum: A new enum +---@param tbl PlenaryEnumRawTable +---@return PlenaryEnumEnum: A new enum local function make_enum(tbl) + ---@type PlenaryEnumEnum local enum = {} + ---@class PlenaryVariant + ---@field value integer local Variant = {} Variant.__index = Variant + ---@param i integer + ---@return PlenaryVariant local function newVariant(i) return setmetatable({ value = i }, Variant) end @@ -60,18 +74,26 @@ local function make_enum(tbl) -- we don't need __eq because the __eq metamethod will only ever be -- invoked when they both have the same metatable + ---@param o PlenaryVariant + ---@return boolean function Variant:__lt(o) return self.value < o.value end + ---@param o PlenaryVariant + ---@return boolean function Variant:__gt(o) return self.value > o.value end + ---@return string function Variant:__tostring() return tostring(self.value) end + ---@param e PlenaryEnumEnum + ---@param i integer + ---@return integer local function find_next_idx(e, i) local newI = i + 1 if not e[newI] then @@ -112,6 +134,9 @@ local function make_enum(tbl) return require("plenary.tbl").freeze(setmetatable(enum, Enum)) end +---@param _ PlenaryEnumEnum +---@param key string|integer +---@return string|PlenaryVariant Enum.__index = function(_, key) if Enum[key] then return Enum[key] @@ -120,8 +145,8 @@ Enum.__index = function(_, key) end --- Checks whether the enum has a member with the given name ---- @param key string: The element to check for ---- @return boolean: True if key is present +---@param key string The element to check for +---@return boolean has_key True if key is present function Enum:has_key(key) if rawget(getmetatable(self).__index, key) then return true @@ -130,8 +155,8 @@ function Enum:has_key(key) end --- If there is a member named 'key', return it, otherwise return nil ---- @param key string: The element to check for ---- @return Variant: The element named by key, or nil if not present +---@param key string The element to check for +---@return PlenaryVariant? variant The element named by key, or nil if not present function Enum:from_str(key) if self:has_key(key) then return self[key] @@ -139,8 +164,8 @@ function Enum:from_str(key) end --- If there is a member of value 'num', return it, otherwise return nil ---- @param num number: The value of the element to check for ---- @return Variant: The element whose value is num +--- @param num integer The value of the element to check for +--- @return PlenaryVariant? variant The element whose value is num function Enum:from_num(num) local key = self[num] if key then @@ -149,8 +174,8 @@ function Enum:from_num(num) end --- Checks whether the given object corresponds to an instance of Enum ---- @param tbl table: The object to be checked ---- @return boolean: True if tbl is an Enum +---@param tbl table The object to be checked +---@return boolean is_enum True if tbl is an Enum local function is_enum(tbl) return getmetatable(getmetatable(tbl).__index) == Enum end @@ -159,4 +184,4 @@ return setmetatable({ is_enum = is_enum, make_enum = make_enum }, { __call = function(_, tbl) return make_enum(tbl) end, -}) +}) --[[@as PlenaryEnum]] diff --git a/lua/plenary/filetype.lua b/lua/plenary/filetype.lua index 60cd30782..17e6a4efd 100644 --- a/lua/plenary/filetype.lua +++ b/lua/plenary/filetype.lua @@ -2,14 +2,20 @@ local Path = require "plenary.path" local os_sep = Path.path.sep +---@class PlenaryFiletype local filetype = {} +---@class PlenaryFiletypeTable +---@field file_name? table +---@field extension? table +---@field shebang? table local filetype_table = { extension = {}, file_name = {}, shebang = {}, } +---@param new_filetypes PlenaryFiletypeTable filetype.add_table = function(new_filetypes) local valid_keys = { "extension", "file_name", "shebang" } local new_keys = {} @@ -39,6 +45,7 @@ filetype.add_table = function(new_filetypes) end end +---@param filename string filetype.add_file = function(filename) local filetype_files = vim.api.nvim_get_runtime_file(string.format("data/plenary/filetypes/%s.lua", filename), true) @@ -51,10 +58,15 @@ filetype.add_file = function(filename) end local filename_regex = "[^" .. os_sep .. "].*" +---@param filename string +---@return string[] filetype._get_extension_parts = function(filename) + ---@type string? local current_match = filename:match(filename_regex) + ---@type string[] local possibilities = {} while current_match do + ---@type string? current_match = current_match:match "[^.]%.(.*)" if current_match then table.insert(possibilities, current_match:lower()) @@ -65,6 +77,8 @@ filetype._get_extension_parts = function(filename) return possibilities end +---@param tail string +---@return string filetype._parse_modeline = function(tail) if tail:find "vim:" then return tail:match ".*:ft=([^: ]*):.*$" or "" @@ -72,6 +86,8 @@ filetype._parse_modeline = function(tail) return "" end +---@param head string +---@return string filetype._parse_shebang = function(head) if head:sub(1, 2) == "#!" then local match = filetype_table.shebang[head:sub(3, #head)] @@ -99,6 +115,8 @@ local extend_tbl_with_ext_eq_ft_entries = function() end end +---@param filepath string +---@return string filetype.detect_from_extension = function(filepath) local exts = filetype._get_extension_parts(filepath) for _, ext in ipairs(exts) do @@ -118,6 +136,8 @@ filetype.detect_from_extension = function(filepath) return "" end +---@param filepath string +---@return string filetype.detect_from_name = function(filepath) if filepath then filepath = filepath:lower() @@ -131,6 +151,8 @@ filetype.detect_from_name = function(filepath) return "" end +---@param filepath string +---@return string? filetype.detect_from_modeline = function(filepath) local tail = Path:new(filepath):readbyterange(-256, 256) if not tail then @@ -143,6 +165,8 @@ filetype.detect_from_modeline = function(filepath) end end +---@param filepath string +---@return string filetype.detect_from_shebang = function(filepath) local head = Path:new(filepath):readbyterange(0, 256) if not head then @@ -152,10 +176,12 @@ filetype.detect_from_shebang = function(filepath) return filetype._parse_shebang(lines[1]) end +---@class PlenaryFiletypeDetectOpts +---@field fs_access boolean Should check a file if it exists (default: `true`) + --- Detect a filetype from a path. ---- ----@param opts table: Table with optional keys ---- - fs_access (bool, default=true): Should check a file if it exists +---@param opts? PlenaryFiletypeDetectOpts +---@return string? filetype.detect = function(filepath, opts) opts = opts or {} opts.fs_access = opts.fs_access or true @@ -164,6 +190,7 @@ filetype.detect = function(filepath, opts) filepath = tostring(filepath) end + ---@type string? local match = filetype.detect_from_name(filepath) if match ~= "" then return match diff --git a/lua/plenary/fun.lua b/lua/plenary/fun.lua index e70367f9e..d13a3f607 100644 --- a/lua/plenary/fun.lua +++ b/lua/plenary/fun.lua @@ -1,7 +1,11 @@ +---@class PlenaryFun local M = {} M.bind = require("plenary.functional").partial +---@param fn fun(...) +---@param argc integer +---@return fun(...) function M.arify(fn, argc) return function(...) if select("#", ...) ~= argc then @@ -12,6 +16,8 @@ function M.arify(fn, argc) end end +---@param map fun(...) +---@return fun(to_wrap: fun(...)): fun(...) function M.create_wrapper(map) return function(to_wrap) return function(...) diff --git a/lua/plenary/functional.lua b/lua/plenary/functional.lua index 6a5b3673d..2402d120e 100644 --- a/lua/plenary/functional.lua +++ b/lua/plenary/functional.lua @@ -1,5 +1,9 @@ +---@class PlenaryFunctional local f = {} +---@generic T, U +---@param t table +---@return { [1]: T, [2]: U }[] function f.kv_pairs(t) local results = {} for k, v in pairs(t) do @@ -8,14 +12,27 @@ function f.kv_pairs(t) return results end +---@generic T, U, V +---@param fun fun(pair: { [1]: T, [2]: U }): V +---@param t table +---@return V[] function f.kv_map(fun, t) return vim.tbl_map(fun, f.kv_pairs(t)) end +---@generic T +---@param array T[] +---@param sep? string +---@return string function f.join(array, sep) return table.concat(vim.tbl_map(tostring, array), sep) end +---@param fn function +---@param n integer +---@param a any +---@param ... any +---@return function local function bind_n(fn, n, a, ...) if n == 0 then return fn @@ -25,10 +42,17 @@ local function bind_n(fn, n, a, ...) end, n - 1, ...) end +---@param fun function +---@param ... any +---@return function function f.partial(fun, ...) return bind_n(fun, select("#", ...), ...) end +---@generic T, U +---@param fun fun(k: T, v: U): boolean +---@param iterable table +---@return boolean function f.any(fun, iterable) for k, v in pairs(iterable) do if fun(k, v) then @@ -39,6 +63,10 @@ function f.any(fun, iterable) return false end +---@generic T, U +---@param fun fun(k: T, v: U): boolean +---@param iterable table +---@return boolean function f.all(fun, iterable) for k, v in pairs(iterable) do if not fun(k, v) then @@ -49,6 +77,11 @@ function f.all(fun, iterable) return true end +---@generic T, U +---@param val any? +---@param was_nil T +---@param was_not_nil U +---@return T|U function f.if_nil(val, was_nil, was_not_nil) if val == nil then return was_nil @@ -57,6 +90,9 @@ function f.if_nil(val, was_nil, was_not_nil) end end +---@generic T +---@param n integer +---@return fun(...: T): T function f.select_only(n) return function(...) local x = select(n, ...) @@ -68,6 +104,9 @@ f.first = f.select_only(1) f.second = f.select_only(2) f.third = f.select_only(3) +---@generic T +---@param ... T +---@return T function f.last(...) local length = select("#", ...) local x = select(length, ...) diff --git a/lua/plenary/init.lua b/lua/plenary/init.lua index 4805a4f6c..560c57a24 100644 --- a/lua/plenary/init.lua +++ b/lua/plenary/init.lua @@ -1,4 +1,26 @@ -- Lazy load everything into plenary. +---@class Plenary +---@field async PlenaryAsync +---@field class PlenaryClass +---@field context_manager PlenaryContextManager +---@field curl PlenaryCurl +---@field enum PlenaryEnum +---@field filetype PlenaryFiletype +---@field fun PlenaryFun +---@field functional PlenaryFunctional +---@field iterators PlenaryIterators +---@field job PlenaryJob +---@field json PlenaryJson +---@field log PlenaryLog +---@field nvim_meta PlenaryNvimMeta +---@field operators PlenaryOperators +---@field path PlenaryPath +---@field reload PlenaryReload +---@field run PlenaryRun +---@field scandir PlenaryScandir +---@field strings PlenaryStrings +---@field tbl PlenaryTbl +---@field window PlenaryWindow local plenary = setmetatable({}, { __index = function(t, k) local ok, val = pcall(require, string.format("plenary.%s", k)) diff --git a/lua/plenary/iterators.lua b/lua/plenary/iterators.lua index 2aac0d20e..75e422399 100644 --- a/lua/plenary/iterators.lua +++ b/lua/plenary/iterators.lua @@ -13,12 +13,17 @@ local compat = require "plenary.compat" -- Tools -------------------------------------------------------------------------------- +---@class PlenaryIterators local exports = {} ----@class Iterator ----@field gen function ----@field param any ----@field state any +---@generic V +---@alias PlenaryIteratorsIterator fun(table: V[], i?: integer): integer, V? + +---@class PlenaryIterator +---@field gen PlenaryIteratorsIterator +---@field param table +---@field state? integer +---@overload fun(param?: table, state?: integer): integer, any? local Iterator = {} Iterator.__index = Iterator @@ -35,6 +40,7 @@ function Iterator:__call(param, state) return self.gen(param or self.param, state or self.state) end +---@return string function Iterator:__tostring() return "" end @@ -87,6 +93,10 @@ local map_gen = function(map, key) return key, key, value end +---@param param string +---@param state integer +---@return integer? state +---@return string? r local string_gen = function(param, state) state = state + 1 if state > #param then @@ -96,6 +106,18 @@ local string_gen = function(param, state) return state, r end +---@generic T: table, U: table +---@alias PlenaryIteratorsRawiterTable fun(obj: T|PlenaryIterator, param?: string, state?: integer): PlenaryIteratorsIterator, T|U|nil, integer? + +---@generic T: table, U: table +---@param obj T|PlenaryIterator +---@param param? string +---@param state? integer +---@return PlenaryIteratorsIterator gen +---@return T|U|nil param +---@return integer? state +---@overload fun(obj: PlenaryIteratorsIterator, param: any, state?: integer): PlenaryIteratorsIterator, any, integer? +---@overload fun(obj: string): PlenaryIteratorsIterator, string?, integer? local rawiter = function(obj, param, state) assert(obj ~= nil, "invalid iterator") @@ -133,7 +155,7 @@ end ---@param gen any ---@param param any ---@param state any ----@return Iterator +---@return PlenaryIterator local function wrap(gen, param, state) return setmetatable({ gen = gen, @@ -143,7 +165,7 @@ local function wrap(gen, param, state) end ---Unwrap an iterator metatable into the iterator triplet ----@param self Iterator +---@param self PlenaryIterator ---@return any ---@return any ---@return any @@ -155,7 +177,7 @@ end ---@param obj any ---@param param any (optional) ---@param state any (optional) ----@return Iterator +---@return PlenaryIterator local iter = function(obj, param, state) return wrap(rawiter(obj, param, state)) end @@ -229,7 +251,7 @@ end ---@param start number ---@param stop number ---@param step number ----@return Iterator +---@return PlenaryIterator local range = function(start, stop, step) if step == nil then if stop == nil then @@ -270,7 +292,7 @@ end ---Creates an infinite iterator that will yield the arguments ---If multiple arguments are passed, the args will be packed and unpacked ---@param ...: the arguments to duplicate ----@return Iterator +---@return PlenaryIterator local duplicate = function(...) if select("#", ...) <= 1 then return wrap(duplicate_gen, select(1, ...), 0) @@ -283,7 +305,7 @@ exports.duplicate = duplicate ---Creates an iterator from a function ---NOTE: if the function is a closure and modifies state, the resulting iterator will not be stateless ---@param fun function ----@return Iterator +---@return PlenaryIterator local from_fun = function(fun) assert(type(fun) == "function") return wrap(duplicate_fun_gen, fun, 0) @@ -292,7 +314,7 @@ exports.from_fun = from_fun ---Creates an infinite iterator that will yield zeros. ---This is an alias to calling duplicate(0) ----@return Iterator +---@return PlenaryIterator local zeros = function() return wrap(duplicate_gen, 0, 0) end @@ -300,7 +322,7 @@ exports.zeros = zeros ---Creates an infinite iterator that will yield ones. ---This is an alias to calling duplicate(1) ----@return Iterator +---@return PlenaryIterator local ones = function() return wrap(duplicate_gen, 1, 0) end @@ -317,7 +339,7 @@ end ---Creates an infinite iterator that will yield random values. ---@param n number ---@param m number ----@return Iterator +---@return PlenaryIterator local rands = function(n, m) if n == nil and m == nil then return wrap(rands_nil_gen, 0, 0) @@ -356,7 +378,7 @@ end ---Return an iterator of substrings separated by a string ---@param input string: the string to split ---@param sep string: the separator to find and split based on ----@return Iterator +---@return PlenaryIterator local split = function(input, sep) return wrap(split_gen, { input, sep }, 1) end @@ -387,7 +409,7 @@ end ---Iterator adapter that maps the previous iterator with a function ---@param fun function: The function to map with. Will be called on each element ----@return Iterator +---@return PlenaryIterator function Iterator:map(fun) return wrap(map_gen2, { self.gen, self.param, fun }, self.state) end @@ -429,7 +451,7 @@ local flatten_gen = function(_, state) end ---Iterator adapter that will recursivley flatten nested iterator structure ----@return Iterator +---@return PlenaryIterator function Iterator:flatten() return wrap(flatten_gen, false, { self.gen, self.param, self.state }) end @@ -482,13 +504,13 @@ end ---Iterator adapter that will filter values ---@param fun function: The function to filter values with. If the function returns true, the value will be kept. ----@return Iterator +---@return PlenaryIterator function Iterator:filter(fun) return wrap(filter_gen, { self.gen, self.param, fun }, self.state) end ---Iterator adapter that will provide numbers from 1 to n as the first multival ----@return Iterator +---@return PlenaryIterator function Iterator:enumerate() local i = 0 return self:map(function(...) @@ -618,7 +640,7 @@ end ---Used for treating consecutive iterators as a single iterator. ---Infinity iterators are supported, but are not recommended. ---@param ...: the iterators to chain ----@return Iterator +---@return PlenaryIterator local chain = function(...) local n = numargs(...) @@ -667,7 +689,7 @@ end ---The returned iterator is truncated in length to the length of the shortest iterator. ---For multi-return iterators only the first variable is used. ---@param ...: the iterators to zip ----@return Iterator +---@return PlenaryIterator local zip = function(...) local n = numargs(...) if n == 0 then diff --git a/lua/plenary/job.lua b/lua/plenary/job.lua index 7f549719d..59356c60c 100644 --- a/lua/plenary/job.lua +++ b/lua/plenary/job.lua @@ -4,8 +4,12 @@ local compat = require "plenary.compat" local F = require "plenary.functional" ----@class Job ----@field command string Command to run +---@alias PlenaryJobCallback fun(error: string, data: string, self?: PlenaryJob) +---@alias PlenaryJobOnExit fun(self: PlenaryJob, code?: integer, signal?: integer) +---@alias PlenaryJobOnOutput fun(err?: string, data?: string, is_complete: boolean) + +---@class PlenaryJobOptions +---@field command? string Command to run ---@field args? string[] List of arguments to pass ---@field cwd? string Working directory for job ---@field env? table|string[] Environment looking like: { ['VAR'] = 'VALUE' } or { 'VAR=VALUE' } @@ -15,15 +19,48 @@ local F = require "plenary.functional" ---@field enable_handlers? boolean If set to false, disables all callbacks associated with output (default: true) ---@field enable_recording? boolean ---@field on_start? fun() ----@field on_stdout? fun(error: string, data: string, self?: Job) ----@field on_stderr? fun(error: string, data: string, self?: Job) ----@field on_exit? fun(self: Job, code: number, signal: number) ----@field maximum_results? number Stop processing results after this number ----@field writer? Job|table|string Job that writes to stdin of this job. +---@field on_stdout? PlenaryJobCallback +---@field on_stderr? PlenaryJobCallback +---@field on_exit? PlenaryJobOnExit +---@field maximum_results? integer Stop processing results after this number +---@field writer? string|PlenaryJob|string[]|uv_pipe_t Job that writes to stdin of this job. +---@field [integer] string Maybe command & args exist here + +---@class PlenaryJob +---@field command string +---@field args string[] +---@field env string[] +---@field interactive boolean +---@field detached? boolean +---@field enable_handlers boolean +---@field enable_recording boolean +---@field handle uv_process_t? +---@field is_shutdown boolean? +---@field stderr? uv_pipe_t +---@field stdin? uv_pipe_t +---@field stdout? uv_pipe_t +---@field user_data table not using +---@field writer? string|PlenaryJob|string[]|uv_pipe_t +---@field _maximum_results? integer +---@field _shutdown_check? uv_check_t +---@field private _additional_on_exit_callbacks PlenaryJobOnExit[] +---@field private _pid (string|integer)? +---@field private _raw_cwd? string +---@field private _stderr_reader? PlenaryJobOnOutput +---@field private _stderr_results? string[] +---@field private _stdout_reader? PlenaryJobOnOutput +---@field private _stdout_results? string[] +---@field private _user_on_exit? PlenaryJobOnExit +---@field private _user_on_start? fun(self: PlenaryJob) +---@field private _user_on_stderr? PlenaryJobCallback +---@field private _user_on_stdout? PlenaryJobCallback local Job = {} Job.__index = Job +---@param j PlenaryJob +---@param key "handle"|"stderr"|"stdin"|"stdout" local function close_safely(j, key) + ---@type uv_handle_t? local handle = j[key] if not handle then @@ -35,6 +72,11 @@ local function close_safely(j, key) end end +---comment +---@param child PlenaryJob +---@param options uv.aliases.spawn_options +---@param code integer +---@param signal integer local start_shutdown_check = function(child, options, code, signal) uv.check_start(child._shutdown_check, function() if not child:_pipes_are_closed(options) then @@ -48,10 +90,14 @@ local start_shutdown_check = function(child, options, code, signal) child:_shutdown(code, signal) -- Remove left over references + ---@diagnostic disable-next-line: cast-local-type child = nil end) end +---@param child PlenaryJob +---@param options uv.aliases.spawn_options +---@return fun(code: integer, signal: integer) local shutdown_factory = function(child, options) return function(code, signal) if uv.is_closing(child._shutdown_check) then @@ -62,6 +108,8 @@ local shutdown_factory = function(child, options) end end +---@param path string +---@return string local function expand(path) if vim.in_fast_event() then return assert(uv.fs_realpath(path), string.format("Path must be valid: %s", path)) @@ -71,15 +119,9 @@ local function expand(path) end end ----@class Array ---- Numeric table - ----@class Map ---- Map-like table - ---Create a new job ----@param o Job ----@return Job +---@param o PlenaryJobOptions +---@return PlenaryJob function Job:new(o) if not o then error(debug.traceback "Options are required for Job:new") @@ -99,7 +141,7 @@ function Job:new(o) local args = o.args if not args then if #o > 1 then - args = { select(2, unpack(o)) } + args = { select(2, unpack(o)) } --[[@as string[] ]] end end @@ -201,7 +243,11 @@ function Job:_stop() close_safely(self, "handle") end +---@param options uv.aliases.spawn_options +---@return boolean function Job:_pipes_are_closed(options) + -- FIX: These handles does not exist. Use options.stdio instead. + ---@diagnostic disable-next-line: undefined-field for _, pipe in ipairs { options.stdin, options.stdout, options.stderr } do if pipe and not uv.is_closing(pipe) then return false @@ -211,7 +257,9 @@ function Job:_pipes_are_closed(options) return true end ---- Shutdown a job. +---Shutdown a job. +---@param code? integer +---@param signal? integer function Job:shutdown(code, signal) if self._shutdown_check and uv.is_active(self._shutdown_check) then -- shutdown has already started @@ -221,6 +269,8 @@ function Job:shutdown(code, signal) self:_shutdown(code, signal) end +---@param code? integer +---@param signal? integer function Job:_shutdown(code, signal) if self.is_shutdown then return @@ -261,6 +311,16 @@ function Job:_shutdown(code, signal) self._stderr_reader = nil end +---Subset type of uv.aliases.spawn_options +---@class PlenaryJobSpawnOptions +---@field command string This does not exist on uv.aliases.spawn_options +---@field args string[] +---@field stdio uv_pipe_t[] +---@field cwd? string +---@field env? table +---@field detached? boolean + +---@return PlenaryJobSpawnOptions function Job:_create_uv_options() local options = {} @@ -282,10 +342,18 @@ function Job:_create_uv_options() return options end +---@param self PlenaryJob +---@param result_key "_stderr_results"|"_stdout_results" +---@param cb? PlenaryJobCallback +---@return PlenaryJobOnOutput local on_output = function(self, result_key, cb) + ---@param err string + ---@param data string + ---@param is_complete boolean return coroutine.wrap(function(err, data, is_complete) local result_index = 1 + ---@type string, integer, string?, boolean local line, start, result_line, found_newline -- We repeat forever as a coroutine so that we can keep calling this. @@ -358,7 +426,7 @@ local on_output = function(self, result_key, cb) -- If we didn't get a newline on the last execute, send the final results. if cb and is_complete and not found_newline then - cb(err, result_line, self) + cb(err, result_line --[[@as string]], self) end if is_complete then @@ -377,10 +445,11 @@ function Job:_prepare_pipes() if self.writer then if Job.is_job(self.writer) then - self.writer:_prepare_pipes() - self.stdin = self.writer.stdout + local writer = self.writer --[[@as PlenaryJob]] + writer:_prepare_pipes() + self.stdin = writer.stdout elseif self.writer.write then - self.stdin = self.writer + self.stdin = self.writer --[[@as uv_pipe_t]] end end @@ -400,7 +469,7 @@ function Job:_execute() self:_user_on_start() end - self.handle, self.pid = uv.spawn(options.command, options, shutdown_factory(self, options)) + self.handle, self._pid = uv.spawn(options.command, options, shutdown_factory(self, options)) if not self.handle then error(debug.traceback("Failed to spawn process: " .. vim.inspect(self))) @@ -429,14 +498,14 @@ function Job:_execute() end) end end - elseif type(self.writer) == "string" then - self.stdin:write(self.writer, function() + elseif type(writer) == "string" then + self.stdin:write(writer, function() self.stdin:close() end) - elseif self.writer.write then - self.stdin = self.writer + elseif writer.write then + self.stdin = writer --[[@as uv_pipe_t]] else - error("Unknown self.writer: " .. vim.inspect(self.writer)) + error("Unknown self.writer: " .. vim.inspect(writer)) end end @@ -449,6 +518,10 @@ function Job:start() self:_execute() end +---@param timeout integer +---@param wait_interval? integer +---@return string[]? result +---@return integer code function Job:sync(timeout, wait_interval) self:start() self:wait(timeout, wait_interval) @@ -456,20 +529,27 @@ function Job:sync(timeout, wait_interval) return self.enable_recording and self:result() or nil, self.code end +---@return string[]? function Job:result() assert(self.enable_recording, "'enable_recording' is not enabled for this job.") return self._stdout_results end +---@return string[]? function Job:stderr_result() assert(self.enable_recording, "'enable_recording' is not enabled for this job.") return self._stderr_results end +---@return (string|integer)? function Job:pid() - return self.pid + return self._pid end +---@param timeout? integer +---@param wait_interval? integer +---@param should_redraw? boolean +---@return PlenaryJob? function Job:wait(timeout, wait_interval, should_redraw) timeout = timeout or 5000 wait_interval = wait_interval or 10 @@ -510,6 +590,9 @@ function Job:wait(timeout, wait_interval, should_redraw) return self end +---@async +---@param wait_time? integer +---@return PlenaryJob? function Job:co_wait(wait_time) wait_time = wait_time or 5 @@ -527,12 +610,16 @@ function Job:co_wait(wait_time) return self end ---- Wait for all jobs to complete +---Wait for all jobs to complete +---@param ... integer|PlenaryJob Jobs to wait for. The last arg can be a timeout. +---@return boolean, -1|-2|nil function Job.join(...) local jobs_to_wait = { ... } + ---@diagnostic disable-next-line: deprecated local num_jobs = table.getn(jobs_to_wait) -- last entry can be timeout + ---@type integer? local timeout if type(jobs_to_wait[num_jobs]) == "number" then timeout = table.remove(jobs_to_wait, num_jobs) @@ -554,25 +641,31 @@ function Job.join(...) end local _request_id = 0 +---@type table local _request_status = {} +---@param next_job PlenaryJob function Job:and_then(next_job) self:add_on_exit_callback(function() next_job:start() end) end +---@param next_job PlenaryJob function Job:and_then_wrap(next_job) self:add_on_exit_callback(vim.schedule_wrap(function() next_job:start() end)) end +---@param fn PlenaryJobOnExit +---@return PlenaryJob function Job:after(fn) self:add_on_exit_callback(fn) return self end +---@param next_job PlenaryJob function Job:and_then_on_success(next_job) self:add_on_exit_callback(function(_, code) if code == 0 then @@ -581,6 +674,7 @@ function Job:and_then_on_success(next_job) end) end +---@param next_job PlenaryJob function Job:and_then_on_success_wrap(next_job) self:add_on_exit_callback(vim.schedule_wrap(function(_, code) if code == 0 then @@ -589,6 +683,7 @@ function Job:and_then_on_success_wrap(next_job) end)) end +---@param fn PlenaryJobOnExit function Job:after_success(fn) self:add_on_exit_callback(function(j, code, signal) if code == 0 then @@ -597,6 +692,7 @@ function Job:after_success(fn) end) end +---@param next_job PlenaryJob function Job:and_then_on_failure(next_job) self:add_on_exit_callback(function(_, code) if code ~= 0 then @@ -605,6 +701,7 @@ function Job:and_then_on_failure(next_job) end) end +---@param next_job PlenaryJob function Job:and_then_on_failure_wrap(next_job) self:add_on_exit_callback(vim.schedule_wrap(function(_, code) if code ~= 0 then @@ -613,6 +710,7 @@ function Job:and_then_on_failure_wrap(next_job) end)) end +---@param fn PlenaryJobOnExit function Job:after_failure(fn) self:add_on_exit_callback(function(j, code, signal) if code ~= 0 then @@ -621,6 +719,8 @@ function Job:after_failure(fn) end) end +---@param ... PlenaryJob +---@return integer _request_id function Job.chain(...) _request_id = _request_id + 1 _request_status[_request_id] = false @@ -650,10 +750,14 @@ function Job.chain(...) return _request_id end +---@param id integer +---@return boolean function Job.chain_status(id) return _request_status[id] end +---@param item any +---@return boolean function Job.is_job(item) if type(item) ~= "table" then return false @@ -662,11 +766,13 @@ function Job.is_job(item) return getmetatable(item) == Job end +---@param cb PlenaryJobOnExit function Job:add_on_exit_callback(cb) table.insert(self._additional_on_exit_callbacks, cb) end ---- Send data to a job. +---Send data to a job. +---@param data string|string[] function Job:send(data) if not self.stdin then error "job has no 'stdin'. Have you run `job:start()` yet?" diff --git a/lua/plenary/json.lua b/lua/plenary/json.lua index 05581f7ba..03b5b0d48 100644 --- a/lua/plenary/json.lua +++ b/lua/plenary/json.lua @@ -2,20 +2,32 @@ local singleComment = "singleComment" local multiComment = "multiComment" +---@return "" local stripWithoutWhitespace = function() return "" end +---@param str string +---@param from? integer +---@param to? integer +---@return string local function slice(str, from, to) from = from or 1 to = to or #str return str:sub(from, to) end +---@param str string +---@param from? integer +---@param to? integer +---@return string, integer count local stripWithWhitespace = function(str, from, to) return slice(str, from, to):gsub("%S", " ") end +---@param jsonString string +---@param quotePosition integer +---@return boolean local isEscaped = function(jsonString, quotePosition) local index = quotePosition - 1 local backslashCount = 0 @@ -27,13 +39,14 @@ local isEscaped = function(jsonString, quotePosition) return backslashCount % 2 == 1 and true or false end +---@class PlenaryJson local M = {} -- Strips any json comments from a json string. -- The resulting string can then be used by `vim.fn.json_decode` -- ---@param jsonString string ----@param options? table +---@param options? { whitespace?: boolean } --- * whitespace: --- - defaults to true --- - when true, comments will be replaced by whitespace @@ -48,6 +61,7 @@ function M.json_strip_comments(jsonString, options) local omitTrailingCommas = not options.trailing_commas local insideString = false + ---@type "multiComment"|"singleComment"|false local insideComment = false local offset = 1 local result = "" diff --git a/lua/plenary/log.lua b/lua/plenary/log.lua index d7d333488..915e301e8 100644 --- a/lua/plenary/log.lua +++ b/lua/plenary/log.lua @@ -9,38 +9,57 @@ local Path = require "plenary.path" +---@type boolean|string|vim.NIL local p_debug = vim.fn.getenv "DEBUG_PLENARY" if p_debug == vim.NIL then p_debug = false end +---@alias PlenaryLogLevel "trace"|"debug"|"info"|"warn"|"error"|"fatal" + +-- luacheck: push ignore 631 + +---Adjust content as needed, but must keep function parameters to be filled +---by library code. +---* param is_console boolean If output is for console or log file. +---* param mode_name string Level configuration 'modes' field 'name' +---* param src_path string Path to source file given by debug.info.source +---* param src_line integer Line into source file given by debug.info.currentline +---* param msg string Message, which is later on escaped, if needed. +---@alias PlenaryLogFmtMsg fun(is_console: boolean, mode_name: string, src_path: string, src_line: integer, msg: string): string + +-- luacheck: pop + +---@class PlenaryLogLevelConfig +---@field name PlenaryLogLevel +---@field hl string highight name for console. + +---@class PlenaryLogConfig +---@field plugin? string Name of the plugin. Prepended to log messages. (default: `plenary`) +---@field use_console? '"async"'|"sync"|false Should print the output to neovim. (default: `"async"`) +---@field highlights? boolean Should highlighting be used in console (using echohl). (default: `true`) +---@field info_level? integer Level of function call stack. (default: `2`) +---@field use_file? boolean Should write to a file. Default logging file is `stdpath("cache")/plugin`. (default: `true`) +---@field outfile? string Output file has precedence over plugin, if not nil and use_file == true. (default: `nil`) +---@field use_quickfix? boolean Should write to the quickfix list. (default: `false`) +---@field level? PlenaryLogLevel Any messages above this level will be logged. (default: `"info"` or `"debug"`) +---@field modes? PlenaryLogLevelConfig[] Level configuration. +---@field float_precision? float Can limit the number of decimals displayed for floats. (default: `0.01`) +---@field fmt_msg? PlenaryLogFmtMsg + -- User configuration section +---@type PlenaryLogConfig local default_config = { - -- Name of the plugin. Prepended to log messages. plugin = "plenary", - - -- Should print the output to neovim while running. - -- values: 'sync','async',false use_console = "async", - - -- Should highlighting be used in console (using echohl). highlights = true, -- Should write to a file. -- Default output for logging file is `stdpath("log")/plugin.log`. use_file = true, - - -- Output file has precedence over plugin, if not nil. - -- Used for the logging file, if not nil and use_file == true. outfile = nil, - - -- Should write to the quickfix list. use_quickfix = false, - - -- Any messages above this level will be logged. level = p_debug and "debug" or "info", - - -- Level configuration. modes = { { name = "trace", hl = "Comment" }, { name = "debug", hl = "Comment" }, @@ -49,17 +68,7 @@ local default_config = { { name = "error", hl = "ErrorMsg" }, { name = "fatal", hl = "ErrorMsg" }, }, - - -- Can limit the number of decimals displayed for floats. float_precision = 0.01, - - -- Adjust content as needed, but must keep function parameters to be filled - -- by library code. - ---@param is_console boolean If output is for console or log file. - ---@param mode_name string Level configuration 'modes' field 'name' - ---@param src_path string Path to source file given by debug.info.source - ---@param src_line integer Line into source file given by debug.info.currentline - ---@param msg string Message, which is later on escaped, if needed. fmt_msg = function(is_console, mode_name, src_path, src_line, msg) local nameupper = mode_name:upper() local lineinfo = src_path .. ":" .. src_line @@ -72,30 +81,64 @@ local default_config = { } -- {{{ NO NEED TO CHANGE + +---@class PlenaryLog +---@field trace fun(...) Write TRACE log. +---@field debug fun(...) Write DEBUG log. +---@field info fun(...) Write INFO log. +---@field warn fun(...) Write WARN log. +---@field error fun(...) Write ERROR log. +---@field fatal fun(...) Write FATAL log. +---@field fmt_trace fun(fmt: string, ...: any) Write TRACE log with formatting by string.format. +---@field fmt_debug fun(fmt: string, ...: any) Write DEBUG log with formatting by string.format. +---@field fmt_info fun(fmt: string, ...: any) Write INFO log with formatting by string.format. +---@field fmt_warn fun(fmt: string, ...: any) Write WARN log with formatting by string.format. +---@field fmt_error fun(fmt: string, ...: any) Write ERROR log with formatting by string.format. +---@field fmt_fatal fun(fmt: string, ...: any) Write FATAL log with formatting by string.format. +---@field lazy_trace fun(heavy_func: fun(...): string) Write TRACE log from output of heavy_func(). +---@field lazy_debug fun(heavy_func: fun(...): string) Write DEBUG log from output of heavy_func(). +---@field lazy_info fun(heavy_func: fun(...): string) Write INFO log from output of heavy_func(). +---@field lazy_warn fun(heavy_func: fun(...): string) Write WARN log from output of heavy_func(). +---@field lazy_error fun(heavy_func: fun(...): string) Write ERROR log from output of heavy_func(). +---@field lazy_fatal fun(heavy_func: fun(...): string) Write FATAL log from output of heavy_func(). +---@field file_trace fun(vals: table, override: PlenaryLogConfig) Write TRACE log into file instead of console. +---@field file_debug fun(vals: table, override: PlenaryLogConfig) Write DEBUG log into file instead of console. +---@field file_info fun(vals: table, override: PlenaryLogConfig) Write INFO log into file instead of console. +---@field file_warn fun(vals: table, override: PlenaryLogConfig) Write WARN log into file instead of console. +---@field file_error fun(vals: table, override: PlenaryLogConfig) Write ERROR log into file instead of console. +---@field file_fatal fun(vals: table, override: PlenaryLogConfig) Write FATAL log into file instead of console. local log = {} local unpack = unpack or table.unpack +---@param config PlenaryLogConfig +---@param standalone boolean +---@return PlenaryLog log.new = function(config, standalone) config = vim.tbl_deep_extend("force", default_config, config) local outfile = vim.F.if_nil( config.outfile, Path:new(vim.api.nvim_call_function("stdpath", { "log" }), config.plugin .. ".log").filename - ) + ) --[[@as string]] + ---@type PlenaryLog local obj if standalone then obj = log else - obj = config + obj = config --[[@as PlenaryLog]] end + ---@type table local levels = {} for i, v in ipairs(config.modes) do levels[v.name] = i end + ---@param x float + ---@param increment? integer + ---@return float local round = function(x, increment) if x == 0 then return x @@ -105,6 +148,8 @@ log.new = function(config, standalone) return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment end + ---@param ... any + ---@return string local make_string = function(...) local t = {} for i = 1, select("#", ...) do @@ -123,6 +168,10 @@ log.new = function(config, standalone) return table.concat(t, " ") end + ---@param level integer + ---@param level_config PlenaryLogLevelConfig + ---@param message_maker fun(...): string + ---@param ... any local log_at_level = function(level, level_config, message_maker, ...) -- Return early if we're below the config.level if level < levels[config.level] then @@ -145,7 +194,7 @@ log.new = function(config, standalone) for _, v in ipairs(split_console) do local formatted_msg = string.format("[%s] %s", config.plugin, vim.fn.escape(v, [["\]])) - local ok = pcall(vim.cmd, string.format([[echom "%s"]], formatted_msg)) + local ok = pcall(vim.cmd --[[@as fun(command: string)]], string.format([[echom "%s"]], formatted_msg)) if not ok then vim.api.nvim_out_write(msg .. "\n") end @@ -190,12 +239,14 @@ log.new = function(config, standalone) end for i, x in ipairs(config.modes) do - -- log.info("these", "are", "separated") + ---log.info("these", "are", "separated") + ---@param ... any obj[x.name] = function(...) return log_at_level(i, x, make_string, ...) end - -- log.fmt_info("These are %s strings", "formatted") + ---log.fmt_info("These are %s strings", "formatted") + ---@param ... any obj[("fmt_%s"):format(x.name)] = function(...) return log_at_level(i, x, function(...) local passed = { ... } @@ -208,14 +259,16 @@ log.new = function(config, standalone) end, ...) end - -- log.lazy_info(expensive_to_calculate) + ---log.lazy_info(expensive_to_calculate) obj[("lazy_%s"):format(x.name)] = function() return log_at_level(i, x, function(f) return f() end) end - -- log.file_info("do not print") + ---log.file_info("do not print") + ---@param vals table + ---@param override PlenaryLogConfig obj[("file_%s"):format(x.name)] = function(vals, override) local original_console = config.use_console config.use_console = false diff --git a/lua/plenary/nvim_meta.lua b/lua/plenary/nvim_meta.lua index 12d9e812b..a7e63914d 100644 --- a/lua/plenary/nvim_meta.lua +++ b/lua/plenary/nvim_meta.lua @@ -1,3 +1,9 @@ +---@class PlenaryNvimMetaLuaVersion +---@field jit string +---@field lua string +---@field version string + +---@return PlenaryNvimMetaLuaVersion local get_lua_version = function() if jit then return { @@ -10,9 +16,13 @@ local get_lua_version = function() error("NEOROCKS: Unsupported Lua Versions", _VERSION) end +---@class PlenaryNvimMeta +---@field is_headless boolean +---@field lua_jit PlenaryNvimMetaLuaVersion + return { -- Is run in `--headless` mode. is_headless = (#vim.api.nvim_list_uis() == 0), lua_jit = get_lua_version(), -} +} --[[@as PlenaryNvimMeta]] diff --git a/lua/plenary/operators.lua b/lua/plenary/operators.lua index ccc00c9f2..8e9027322 100644 --- a/lua/plenary/operators.lua +++ b/lua/plenary/operators.lua @@ -4,7 +4,8 @@ ---Lua has no currying so we have to make a function for each operator. ---@brief ]] -return { +---@class PlenaryOperators +local M = { ---------------------------------------------------------------------------- -- Comparison operators ---------------------------------------------------------------------------- @@ -30,15 +31,27 @@ return { ---------------------------------------------------------------------------- -- Arithmetic operators ---------------------------------------------------------------------------- + ---@param a number + ---@param b number + ---@return number add = function(a, b) return a + b end, + ---@param a number + ---@param b number + ---@return number div = function(a, b) return a / b end, + ---@param a number + ---@param b number + ---@return integer floordiv = function(a, b) return math.floor(a / b) end, + ---@param a number + ---@param b number + ---@return integer intdiv = function(a, b) local q = a / b if a >= 0 then @@ -47,24 +60,43 @@ return { return math.ceil(q) end end, + ---@param a number + ---@param b number + ---@return number mod = function(a, b) return a % b end, + ---@param a number + ---@param b number + ---@return number mul = function(a, b) return a * b end, + ---@param a number + ---@return number neq = function(a) return -a end, + ---@param a number + ---@return number unm = function(a) return -a end, -- an alias + ---@param a number + ---@param b number + ---@return number pow = function(a, b) return a ^ b end, + ---@param a number + ---@param b number + ---@return number sub = function(a, b) return a - b end, + ---@param a number + ---@param b number + ---@return number truediv = function(a, b) return a / b end, @@ -72,12 +104,19 @@ return { ---------------------------------------------------------------------------- -- String operators ---------------------------------------------------------------------------- + ---@param a string + ---@param b string + ---@return string concat = function(a, b) return a .. b end, + ---@param a string + ---@return integer len = function(a) return #a end, + ---@param a string + ---@return integer length = function(a) return #a end, -- an alias @@ -85,16 +124,28 @@ return { ---------------------------------------------------------------------------- -- Logical operators ---------------------------------------------------------------------------- + ---@param a boolean + ---@param b boolean + ---@return boolean land = function(a, b) return a and b end, + ---@param a boolean + ---@param b boolean + ---@return boolean lor = function(a, b) return a or b end, + ---@param a boolean + ---@return boolean lnot = function(a) return not a end, + ---@param a boolean + ---@return boolean truth = function(a) return not not a end, } + +return M diff --git a/lua/plenary/path.lua b/lua/plenary/path.lua index 0865f2e35..7add76bb5 100644 --- a/lua/plenary/path.lua +++ b/lua/plenary/path.lua @@ -31,12 +31,15 @@ path.sep = (function() end end)() +---@type fun(base?: string): string path.root = (function() if path.sep == "/" then return function() return "/" end else + ---@param base string + ---@return string return function(base) base = base or vim.loop.cwd() return base:sub(1, 1) .. ":\\" @@ -46,24 +49,33 @@ end)() path.S_IF = S_IF +---@param reg integer +---@param value integer +---@return boolean local band = function(reg, value) return bit.band(reg, value) == reg end +---@param ... string +---@return string local concat_paths = function(...) return table.concat({ ... }, path.sep) end +---@param pathname string +---@return boolean local function is_root(pathname) if path.sep == "\\" then - return string.match(pathname, "^[A-Z]:\\?$") + return string.match(pathname, "^[A-Z]:\\?$") ~= nil end return pathname == "/" end +---@return fun(filepath: string): string[] local _split_by_separator = (function() local formatted = string.format("([^%s]+)", path.sep) return function(filepath) + ---@type string[] local t = {} for str in string.gmatch(filepath, formatted) do table.insert(t, str) @@ -72,10 +84,15 @@ local _split_by_separator = (function() end end)() +---@param filename string +---@return boolean local is_uri = function(filename) return string.match(filename, "^%a[%w+-.]*://") ~= nil end +---@param filename string +---@param sep string +---@return boolean local is_absolute = function(filename, sep) if sep == "\\" then return string.match(filename, "^[%a]:[\\/].*$") ~= nil @@ -83,6 +100,9 @@ local is_absolute = function(filename, sep) return string.sub(filename, 1, 1) == sep end +---@param filename string +---@param cwd string +---@return string local function _normalize_path(filename, cwd) if is_uri(filename) then return filename @@ -100,6 +120,8 @@ local function _normalize_path(filename, cwd) if has then local is_abs = is_absolute(filename, path.sep) + ---@param filename_local string + ---@return string[] local split_without_disk_name = function(filename_local) local parts = _split_by_separator(filename_local) -- Remove disk name part on Windows @@ -140,6 +162,8 @@ local function _normalize_path(filename, cwd) return out_file end +---@param pathname string +---@return string local clean = function(pathname) if is_uri(pathname) then return pathname @@ -161,11 +185,17 @@ end -- S_IFLNK = 0o120000 # symbolic link -- S_IFSOCK = 0o140000 # socket file ----@class Path +---@class PlenaryPath +---@field _absolute? string +---@field _cwd? string +---@field _sep string +---@field filename string local Path = { path = path, } +---@param self PlenaryPath|string +---@return PlenaryPath local check_self = function(self) if type(self) == "string" then return Path:new(self) @@ -174,6 +204,9 @@ local check_self = function(self) return self end +---@param t PlenaryPath +---@param k string +---@return any Path.__index = function(t, k) local raw = rawget(Path, k) if raw then @@ -196,6 +229,9 @@ end -- TODO: Could use this to not have to call new... not sure -- Path.__call = Path:new +---@param self PlenaryPath +---@param other PlenaryPath|string +---@return PlenaryPath Path.__div = function(self, other) assert(Path.is_path(self)) assert(Path.is_path(other) or type(other) == "string") @@ -203,19 +239,49 @@ Path.__div = function(self, other) return self:joinpath(other) end +---@param self PlenaryPath +---@return string Path.__tostring = function(self) return clean(self.filename) end -- TODO: See where we concat the table, and maybe we could make this work. +---@param self PlenaryPath +---@param other PlenaryPath|string +---@return string Path.__concat = function(self, other) return self.filename .. other end +---@param a any +---@return boolean Path.is_path = function(a) return getmetatable(a) == Path end +---Path:new() is a constructor for PlenaryPath. This accepts `string` or +---`PlenaryPath` as its arguments. +--- +---```lua +---local p1 = Path:new("path/to", "some_file") +--- +---local p2 = Path:new { "path/to", "some_file" } +--- +---local parent = Path:new "path/to" +---local p3 = Path:new { parent, "some_file" } +---``` +--- +---`p1`, `p2`, `p3` have the same filename: `path/to/some_file`. +--- +---`sep` property can be used to create objects that has different separator +---from the system's one. +--- +---```lua +---local p4 = Path:new { "path/to", "some_file", sep = "\\" } +---``` +--- +---This has `path\to\some_file` as its filename. +---@type fun(...: string|(string|PlenaryPath)[]|PlenaryPath): PlenaryPath function Path:new(...) local args = { ... } @@ -224,34 +290,39 @@ function Path:new(...) self = Path -- luacheck: ignore end + ---@type string|(string|PlenaryPath)[]|PlenaryPath local path_input if #args == 1 then - path_input = args[1] + path_input = args[1] --[[@as string|PlenaryPath]] else - path_input = args + path_input = args --[[@as (string|PlenaryPath)[] ]] end -- If we already have a Path, it's fine. -- Just return it if Path.is_path(path_input) then - return path_input + return path_input --[[@as PlenaryPath]] end -- TODO: Should probably remove and dumb stuff like double seps, periods in the middle, etc. local sep = path.sep if type(path_input) == "table" then sep = path_input.sep or path.sep + ---@diagnostic disable-next-line: inject-field path_input.sep = nil end + ---@type string local path_string if type(path_input) == "table" then -- TODO: It's possible this could be done more elegantly with __concat -- But I'm unsure of what we'd do to make that happen + + ---@type string[] local path_objs = {} for _, v in ipairs(path_input) do if Path.is_path(v) then - table.insert(path_objs, v.filename) + table.insert(path_objs, (v --[[@as PlenaryPath]]).filename) else assert(type(v) == "string") table.insert(path_objs, v) @@ -264,6 +335,7 @@ function Path:new(...) path_string = path_input end + ---@type PlenaryPath local obj = { filename = path_string, @@ -275,10 +347,12 @@ function Path:new(...) return obj end +---@return string function Path:_fs_filename() return self:absolute() or self.filename end +---@return uv.aliases.fs_stat_table function Path:_stat() return uv.fs_stat(self:_fs_filename()) or {} -- local stat = uv.fs_stat(self:absolute()) @@ -291,14 +365,18 @@ function Path:_stat() -- return self._stat_result end +---@return integer function Path:_st_mode() return self:_stat().mode or 0 end +---@param ... PlenaryPath|string +---@return PlenaryPath function Path:joinpath(...) return Path:new(self.filename, ...) end +---@return string function Path:absolute() if self:is_absolute() then return _normalize_path(self.filename, self._cwd) @@ -307,10 +385,12 @@ function Path:absolute() end end +---@return boolean function Path:exists() return not vim.tbl_isempty(self:_stat()) end +---@return string function Path:expand() if is_uri(self.filename) then return self.filename @@ -339,6 +419,8 @@ function Path:expand() return expanded and expanded or error "Path not valid" end +---@param cwd string +---@return string function Path:make_relative(cwd) if is_uri(self.filename) then return self.filename @@ -361,6 +443,8 @@ function Path:make_relative(cwd) return self.filename end +---@param cwd string +---@return string function Path:normalize(cwd) if is_uri(self.filename) then return self.filename @@ -383,6 +467,10 @@ function Path:normalize(cwd) return _normalize_path(clean(self.filename), self._cwd) end +---@param filename string +---@param len integer +---@param exclude? integer[] +---@return string local function shorten_len(filename, len, exclude) len = len or 1 exclude = exclude or { -1 } @@ -430,6 +518,7 @@ local function shorten_len(filename, len, exclude) return table.concat(final_path_components) end +---@return fun(filenamne: string): string local shorten = (function() local fallback = function(filename) return shorten_len(filename, 1) @@ -461,6 +550,9 @@ local shorten = (function() return fallback end)() +---@param len? integer +---@param exclude? integer[] +---@return string function Path:shorten(len, exclude) assert(len ~= 0, "len must be at least 1") if (len and len > 1) or exclude ~= nil then @@ -469,6 +561,13 @@ function Path:shorten(len, exclude) return shorten(self.filename) end +---@class PathMkdirOpts +---@field mode? integer +---@field parents? boolean +---@field exists_ok? boolean + +---@param opts? PathMkdirOpts +---@return boolean function Path:mkdir(opts) opts = opts or {} @@ -523,6 +622,11 @@ function Path:rmdir() uv.fs_rmdir(self:absolute()) end +---@class PathRenameOpts +---@field new_name string + +---@param opts? PathRenameOpts +---@return boolean|nil function Path:rename(opts) opts = opts or {} if not opts.new_name or opts.new_name == "" then @@ -549,22 +653,25 @@ function Path:rename(opts) return status end +---@class PathCopyOpts +---@field destination string|PlenaryPath target file path to copy to +---@field recursive boolean whether to copy folders recursively (default: false) +---@field override boolean whether to override files (default: true) +---@field interactive boolean confirm if copy would override; precedes `override` (default: false) +---@field respect_gitignore boolean skip folders ignored by all detected `gitignore`s (default: false) +---@field hidden boolean whether to add hidden files in recursively copying folders (default: true) +---@field parents boolean whether to create possibly non-existing parent dirs of `opts.destination` (default: false) +---@field exists_ok boolean whether ok if `opts.destination` exists, if so folders are merged (default: true) + --- Copy files or folders with defaults akin to GNU's `cp`. ----@param opts table: options to pass to toggling registered actions ----@field destination string|Path: target file path to copy to ----@field recursive bool: whether to copy folders recursively (default: false) ----@field override bool: whether to override files (default: true) ----@field interactive bool: confirm if copy would override; precedes `override` (default: false) ----@field respect_gitignore bool: skip folders ignored by all detected `gitignore`s (default: false) ----@field hidden bool: whether to add hidden files in recursively copying folders (default: true) ----@field parents bool: whether to create possibly non-existing parent dirs of `opts.destination` (default: false) ----@field exists_ok bool: whether ok if `opts.destination` exists, if so folders are merged (default: true) ----@return table {[Path of destination]: bool} indicating success of copy; nested tables constitute sub dirs +---@param opts? PathCopyOpts options to pass to toggling registered actions +---@return table success indicating success of copy; nested tables constitute sub dirs function Path:copy(opts) opts = opts or {} opts.recursive = F.if_nil(opts.recursive, false, opts.recursive) opts.override = F.if_nil(opts.override, true, opts.override) + ---@type PlenaryPath local dest = opts.destination -- handles `.`, `..`, `./`, and `../` if not Path.is_path(dest) then @@ -577,6 +684,7 @@ function Path:copy(opts) dest = Path:new(dest) end -- success is true in case file is copied, false otherwise + ---@type table local success = {} if not self:is_dir() then if opts.interactive and dest:exists() then @@ -622,6 +730,12 @@ function Path:copy(opts) end end +---@class PathTouchOpts +---@field mode? integer +---@field parents? boolean + +---@param opts PathTouchOpts +---@return boolean|nil function Path:touch(opts) opts = opts or {} @@ -647,6 +761,10 @@ function Path:touch(opts) return true end +---@class PathRmOpts +---@field recursive? boolean + +---@param opts PathRmOpts function Path:rm(opts) opts = opts or {} @@ -677,6 +795,8 @@ function Path:rm(opts) end -- Path:is_* {{{ + +---@return boolean function Path:is_dir() -- TODO: I wonder when this would be better, if ever. -- return self:_stat().type == 'directory' @@ -684,18 +804,23 @@ function Path:is_dir() return band(S_IF.DIR, self:_st_mode()) end +---@return boolean function Path:is_absolute() return is_absolute(self.filename, self._sep) end -- }}} +---@return string[] function Path:_split() return vim.split(self:absolute(), self._sep) end local _get_parent = (function() local formatted = string.format("^(.+)%s[^%s]+", path.sep, path.sep) + ---@param abs_path string + ---@return string? return function(abs_path) + ---@type string? local parent = abs_path:match(formatted) if parent ~= nil and not parent:find(path.sep) then return parent .. path.sep @@ -704,13 +829,17 @@ local _get_parent = (function() end end)() +---@return PlenaryPath function Path:parent() return Path:new(_get_parent(self:absolute()) or path.root(self:absolute())) end +---@return string[] function Path:parents() local results = {} - local cur = self:absolute() + ---@type string? + local cur + cur = self:absolute() repeat cur = _get_parent(cur) table.insert(results, cur) @@ -719,6 +848,7 @@ function Path:parents() return results end +---@return boolean|nil function Path:is_file() return self:_stat().type == "file" and true or nil end @@ -729,6 +859,9 @@ function Path:open() end function Path:close() end +---@param txt string +---@param flag uv.aliases.fs_access_flags|integer +---@param mode? integer function Path:write(txt, flag, mode) assert(flag, [[Path:write_text requires a flag! For example: 'w' or 'a']]) @@ -741,25 +874,29 @@ end -- TODO: Asyncify this and use vim.wait in the meantime. -- This will allow other events to happen while we're waiting! +---@return string function Path:_read() self = check_self(self) local fd = assert(uv.fs_open(self:_fs_filename(), "r", 438)) -- for some reason test won't pass with absolute local stat = assert(uv.fs_fstat(fd)) - local data = assert(uv.fs_read(fd, stat.size, 0)) + local data = assert(uv.fs_read(fd, stat.size, 0)) --[[@as string]] assert(uv.fs_close(fd)) return data end +---@param callback fun(data?: string): nil function Path:_read_async(callback) vim.loop.fs_open(self.filename, "r", 438, function(err_open, fd) if err_open then print("We tried to open this file but couldn't. We failed with following error message: " .. err_open) return end + assert(fd) vim.loop.fs_fstat(fd, function(err_fstat, stat) assert(not err_fstat, err_fstat) + assert(stat) if stat.type ~= "file" then return callback "" end @@ -774,6 +911,8 @@ function Path:_read_async(callback) end) end +---@param callback? fun(data?: string): nil +---@return string? function Path:read(callback) if callback then return self:_read_async(callback) @@ -781,6 +920,8 @@ function Path:read(callback) return self:_read() end +---@param lines integer? +---@return string? function Path:head(lines) lines = lines or 10 self = check_self(self) @@ -799,7 +940,7 @@ function Path:head(lines) local data = "" local index, count = 0, 0 while count < lines and index < stat.size do - local read_chunk = assert(uv.fs_read(fd, chunk_size, index)) + local read_chunk = assert(uv.fs_read(fd, chunk_size, index)) --[[@as string]] local i = 0 for char in read_chunk:gmatch "." do @@ -824,6 +965,8 @@ function Path:head(lines) return data end +---@param lines? integer +---@return string? function Path:tail(lines) lines = lines or 10 self = check_self(self) @@ -848,7 +991,7 @@ function Path:tail(lines) real_index = 0 end - local read_chunk = assert(uv.fs_read(fd, chunk_size, real_index)) + local read_chunk = assert(uv.fs_read(fd, chunk_size, real_index)) --[[@as string]] local i = #read_chunk while i > 0 do @@ -869,15 +1012,20 @@ function Path:tail(lines) return data end +---@type fun(self: string|PlenaryPath): string[] function Path:readlines() self = check_self(self) local data = self:read() + if not data then + return {} + end data = data:gsub("\r", "") return vim.split(data, "\n") end +---@return fun(): string? function Path:iter() local data = self:readlines() local i = 0 @@ -890,6 +1038,9 @@ function Path:iter() end end +---@param offset integer +---@param length integer +---@return string? function Path:readbyterange(offset, length) self = check_self(self) @@ -927,6 +1078,8 @@ function Path:readbyterange(offset, length) return data end +---@param filename string +---@return PlenaryPath|string function Path:find_upwards(filename) local folder = Path:new(self) local root = path.root(folder:absolute()) diff --git a/lua/plenary/reload.lua b/lua/plenary/reload.lua index 618952201..037a65495 100644 --- a/lua/plenary/reload.lua +++ b/lua/plenary/reload.lua @@ -1,5 +1,8 @@ +---@class PlenaryReload local reload = {} +---@param module_name string +---@param starts_with_only? boolean reload.reload_module = function(module_name, starts_with_only) -- Default to starts with only if starts_with_only == nil then @@ -9,17 +12,22 @@ reload.reload_module = function(module_name, starts_with_only) -- TODO: Might need to handle cpath / compiled lua packages? Not sure. local matcher if not starts_with_only then + ---@param pack string + ---@return integer? matcher = function(pack) return string.find(pack, module_name, 1, true) end else local module_name_pattern = vim.pesc(module_name) + ---@param pack string + ---@return integer? matcher = function(pack) return string.find(pack, "^" .. module_name_pattern) end end -- Handle impatient.nvim automatically. + ---@diagnostic disable-next-line: undefined-field local luacache = (_G.__luacache or {}).cache for pack, _ in pairs(package.loaded) do diff --git a/lua/plenary/run.lua b/lua/plenary/run.lua index 0661fc620..c70a09124 100644 --- a/lua/plenary/run.lua +++ b/lua/plenary/run.lua @@ -1,7 +1,13 @@ local floatwin = require "plenary.window.float" +---@class PlenaryRun local run = {} +---@param title_text string[] +---@param cmd string|string[] +---@param opts? any +---@return integer bufnr +---@return integer win_id run.with_displayed_output = function(title_text, cmd, opts) local views = floatwin.centered_with_top_win(title_text) diff --git a/lua/plenary/scandir.lua b/lua/plenary/scandir.lua index d5b60ee41..32e567559 100644 --- a/lua/plenary/scandir.lua +++ b/lua/plenary/scandir.lua @@ -5,8 +5,13 @@ local compat = require "plenary.compat" local uv = vim.loop +---@class PlenaryScandir local m = {} +---@alias PlenaryScandirGitignore fun(bp: string[], entry: string): boolean + +---@param basepath string[] +---@return PlenaryScandirGitignore? local make_gitignore = function(basepath) local patterns = {} local valid = false @@ -67,6 +72,10 @@ end -- exposed for testing m.__make_gitignore = make_gitignore +---@param base_paths string[] +---@param entry string +---@param depth integer +---@return string? local handle_depth = function(base_paths, entry, depth) for _, v in ipairs(base_paths) do if entry:find(v, 1, true) then @@ -81,6 +90,8 @@ local handle_depth = function(base_paths, entry, depth) return entry end +---@param pattern string|string[]|function +---@return (fun(entry: string): ...)? local gen_search_pat = function(pattern) if type(pattern) == "string" then return function(entry) @@ -100,6 +111,16 @@ local gen_search_pat = function(pattern) end end +---@param opts PlenaryScandirOpts +---@param name string +---@param typ? string +---@param current_dir string +---@param next_dir string[] +---@param bp string[] +---@param data string[] +---@param giti? PlenaryScandirGitignore +---@param msp? fun(entry: string): boolean +---@return nil local process_item = function(opts, name, typ, current_dir, next_dir, bp, data, giti, msp) if opts.hidden or name:sub(1, 1) ~= "." then if typ == "directory" then @@ -133,26 +154,41 @@ local process_item = function(opts, name, typ, current_dir, next_dir, bp, data, end end ---- m.scan_dir --- Search directory recursive and syncronous --- @param path: string or table --- string has to be a valid path --- table has to be a array of valid paths --- @param opts: table to change behavior --- opts.hidden (bool): if true hidden files will be added --- opts.add_dirs (bool): if true dirs will also be added to the results --- opts.only_dirs (bool): if true only dirs will be added to the results --- opts.respect_gitignore (bool): if true will only add files that are not ignored by the git --- opts.depth (int): depth on how deep the search should go --- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or fn(e) -> bool --- opts.on_insert(entry): Will be called for each element --- opts.silent (bool): if true will not echo messages that are not accessible --- @return array with files +---@class PlenaryScandirOpts +---@field add_dirs? boolean if true dirs will also be added to the results +---@field depth? integer depth on how deep the search should go +---@field hidden? boolean if true hidden files will be added +---@field only_dirs? boolean if true only dirs will be added to the results +---@field on_insert? fun(entry: string, typ: string): nil Will be called for each element +---@field respect_gitignore? boolean if true will only add files that are not ignored by the git +---@field search_pattern? string regex for which files will be added, string, table of strings, or fn(e) -> bool +---@field silent? boolean if true will not echo messages that are not accessible + +---m.scan_dir +---Search directory recursive and syncronous +---* param path: string or table +--- string has to be a valid path +--- table has to be a array of valid paths +---* param opts: table to change behavior +--- opts.hidden (bool): if true hidden files will be added +--- opts.add_dirs (bool): if true dirs will also be added to the results +--- opts.only_dirs (bool): if true only dirs will be added to the results +--- opts.respect_gitignore (bool): if true will only add files that are not ignored by the git +--- opts.depth (int): depth on how deep the search should go +--- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or fn(e) -> bool +--- opts.on_insert(entry): Will be called for each element +--- opts.silent (bool): if true will not echo messages that are not accessible +---@param path string|string[] +---@param opts PlenaryScandirOpts +---@return string[] data array with files m.scan_dir = function(path, opts) opts = opts or {} + ---@type string[] local data = {} + ---@type string[] local base_paths = compat.flatten { path } + ---@type string[] local next_dir = compat.flatten { path } local gitignore = opts.respect_gitignore and make_gitignore(base_paths) or nil @@ -171,6 +207,7 @@ m.scan_dir = function(path, opts) end repeat + ---@type string local current_dir = table.remove(next_dir, 1) local fd = uv.fs_scandir(current_dir) if fd then @@ -186,27 +223,37 @@ m.scan_dir = function(path, opts) return data end ---- m.scan_dir_async --- Search directory recursive and asyncronous --- @param path: string or table --- string has to be a valid path --- table has to be a array of valid paths --- @param opts: table to change behavior --- opts.hidden (bool): if true hidden files will be added --- opts.add_dirs (bool): if true dirs will also be added to the results --- opts.only_dirs (bool): if true only dirs will be added to the results --- opts.respect_gitignore (bool): if true will only add files that are not ignored by git --- opts.depth (int): depth on how deep the search should go --- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or fn(e) -> bool --- opts.on_insert function(entry): will be called for each element --- opts.on_exit function(results): will be called at the end --- opts.silent (bool): if true will not echo messages that are not accessible +---@class PlenaryScandirAsyncOpts: PlenaryScandirOpts +---@field on_exit? fun(data: string[]): nil will be called at the end + +---m.scan_dir_async +---Search directory recursive and asyncronous +---* param path: string or table +--- string has to be a valid path +--- table has to be a array of valid paths +---* param opts: table to change behavior +--- opts.hidden (bool): if true hidden files will be added +--- opts.add_dirs (bool): if true dirs will also be added to the results +--- opts.only_dirs (bool): if true only dirs will be added to the results +--- opts.respect_gitignore (bool): if true will only add files that are not ignored by git +--- opts.depth (int): depth on how deep the search should go +--- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or fn(e) -> bool +--- opts.on_insert function(entry): will be called for each element +--- opts.on_exit function(results): will be called at the end +--- opts.silent (bool): if true will not echo messages that are not accessible +---@param path string|string[] +---@param opts PlenaryScandirAsyncOpts +---@return string[]? m.scan_dir_async = function(path, opts) opts = opts or {} + ---@type string[] local data = {} + ---@type string[] local base_paths = compat.flatten { path } + ---@type string[] local next_dir = compat.flatten { path } + ---@type string local current_dir = table.remove(next_dir, 1) -- TODO(conni2461): get gitignore is not async @@ -228,6 +275,8 @@ m.scan_dir_async = function(path, opts) end local read_dir + ---@param err string? + ---@param fd uv_fs_t read_dir = function(err, fd) if not err then while true do @@ -251,6 +300,8 @@ m.scan_dir_async = function(path, opts) end local gen_permissions = (function() + ---@param nr integer + ---@return integer local conv_to_octal = function(nr) local octal, i = 0, 1 @@ -267,6 +318,9 @@ local gen_permissions = (function() local permissions_tbl = { [0] = "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx" } local bit_tbl = { 4, 2, 1 } + ---@param cache table + ---@param mode integer + ---@return string return function(cache, mode) if cache[mode] then return cache[mode] @@ -293,6 +347,8 @@ end)() local gen_size = (function() local size_types = { "", "K", "M", "G", "T", "P", "E", "Z" } + ---@param size number + ---@return string return function(size) -- TODO(conni2461): If type directory we could just return 4.0K for _, v in ipairs(size_types) do @@ -311,15 +367,20 @@ end)() local gen_date = (function() local current_year = os.date "%Y" + ---@param mtime integer + ---@return string return function(mtime) if current_year ~= os.date("%Y", mtime) then - return os.date("%b %d %Y", mtime) + return os.date("%b %d %Y", mtime) --[[@as string]] end - return os.date("%b %d %H:%M", mtime) + return os.date("%b %d %H:%M", mtime) --[[@as string]] end end)() local get_username = (function() + ---@param tbl? table + ---@param id integer + ---@return string|integer local fallback = function(tbl, id) if not tbl then return id @@ -352,10 +413,14 @@ local get_username = (function() passwd *getpwuid(uid_t uid); ]] + ---@param tbl table + ---@param id integer + ---@return string local ffi_func = function(tbl, id) if tbl[id] then return tbl[id] end + ---@type { pw_name: string }? local struct = ffi.C.getpwuid(id) local name if struct == nil then @@ -379,6 +444,9 @@ local get_username = (function() end)() local get_groupname = (function() + ---@param tbl? table + ---@param id integer + ---@return string|integer local fallback = function(tbl, id) if not tbl then return id @@ -405,10 +473,14 @@ local get_groupname = (function() group *getgrgid(gid_t gid); ]] + ---@param tbl table + ---@param id integer + ---@return string local ffi_func = function(tbl, id) if tbl[id] then return tbl[id] end + ---@type { gr_name: string }? local struct = ffi.C.getgrgid(id) local name if struct == nil then @@ -430,6 +502,9 @@ local get_groupname = (function() end end)() +---@generic T +---@param tbl? T[][] +---@return integer local get_max_len = function(tbl) if not tbl then return 0 @@ -443,11 +518,23 @@ local get_max_len = function(tbl) return max_len end +---@class PlenaryScandirLsSection +---@field start_index integer +---@field end_index integer + +---@param data? string[] +---@param path string +---@param opts? PlenaryScandirLsOpts +---@return string[] results +---@return PlenaryScandirLsSection[][] sections local gen_ls = function(data, path, opts) if not data or #data == 0 then return {}, {} end + ---@param per string + ---@param file string + ---@return string local check_link = function(per, file) if per:sub(1, 1) == "l" then local resolved = uv.fs_realpath(path .. os_sep .. file) @@ -462,11 +549,15 @@ local gen_ls = function(data, path, opts) return file end + ---@type string[], PlenaryScandirLsSection[][] local results, sections = {}, {} + ---@type table? local users_tbl = os_sep ~= "\\" and {} or nil + ---@type table? local groups_tbl = os_sep ~= "\\" and {} or nil + ---@type table, table local stats, permissions_cache = {}, {} for _, v in ipairs(data) do local stat = uv.fs_lstat(v) @@ -479,10 +570,14 @@ local gen_ls = function(data, path, opts) local insert_in_results = (function() if not users_tbl and not groups_tbl then + ---@type table local section_spacing_tbl = { [5] = 2, [6] = 0 } + ---@param ... string + ---@return nil return function(...) local args = { ... } + ---@type PlenaryScandirLsSection[] local section = { { start_index = 01, end_index = 11 }, -- permissions, hardcoded indexes { start_index = 12, end_index = 17 }, -- size, hardcoded indexes @@ -504,6 +599,7 @@ local gen_ls = function(data, path, opts) local max_user_len = get_max_len(users_tbl) local max_group_len = get_max_len(groups_tbl) + ---@type table local section_spacing_tbl = { [3] = { max = max_user_len, add = 1 }, [4] = { max = max_group_len, add = 2 }, @@ -512,8 +608,11 @@ local gen_ls = function(data, path, opts) } local fmt_str = "%10s %5s %-" .. max_user_len .. "s %-" .. max_group_len .. "s %s %s" + ---@param ... string + ---@return nil return function(...) local args = { ... } + ---@type PlenaryScandirLsSection[] local section = { { start_index = 01, end_index = 11 }, -- permissions, hardcoded indexes { start_index = 12, end_index = 17 }, -- size, hardcoded indexes @@ -550,7 +649,9 @@ local gen_ls = function(data, path, opts) end if opts and opts.group_directories_first then + ---@type string[] local sorted_results = {} + ---@type PlenaryScandirLsSection[][] local sorted_sections = {} for k, v in ipairs(results) do if v:sub(1, 1) == "d" then @@ -570,37 +671,53 @@ local gen_ls = function(data, path, opts) end end ---- m.ls --- List directory contents. Will always apply --long option. Use scan_dir for without --long --- @param path: string --- string has to be a valid path --- @param opts: table to change behavior --- opts.hidden (bool): if true hidden files will be added --- opts.add_dirs (bool): if true dirs will also be added to the results, default: true --- opts.respect_gitignore (bool): if true will only add files that are not ignored by git --- opts.depth (int): depth on how deep the search should go, default: 1 --- opts.group_directories_first (bool): same as real ls --- @return array with formatted output +---@class PlenaryScandirLsOpts +---@field add_dirs boolean if true dirs will also be added to the results, default: true +---@field depth integer depth on how deep the search should go, default: 1 +---@field group_directories_first boolean same as real ls +---@field hidden boolean if true hidden files will be added +---@field respect_gitignore boolean if true will only add files that are not ignored by git + +---m.ls +---List directory contents. Will always apply --long option. Use scan_dir for without --long +---* param path: string +--- string has to be a valid path +---* param opts: table to change behavior +--- opts.hidden (bool): if true hidden files will be added +--- opts.add_dirs (bool): if true dirs will also be added to the results, default: true +--- opts.respect_gitignore (bool): if true will only add files that are not ignored by git +--- opts.depth (int): depth on how deep the search should go, default: 1 +--- opts.group_directories_first (bool): same as real ls +---* return array with formatted output +---@param path string +---@param opts PlenaryScandirLsOpts +---@return string[] results array with formatted output +---@return PlenaryScandirLsSection[][] sections array with formatted output m.ls = function(path, opts) opts = opts or {} opts.depth = opts.depth or 1 opts.add_dirs = opts.add_dirs or true - local data = m.scan_dir(path, opts) + local data = m.scan_dir(path, opts --[[@as PlenaryScandirOpts]]) return gen_ls(data, path, opts) end ---- m.ls_async --- List directory contents. Will always apply --long option. Use scan_dir for without --long --- @param path: string --- string has to be a valid path --- @param opts: table to change behavior --- opts.hidden (bool): if true hidden files will be added --- opts.add_dirs (bool): if true dirs will also be added to the results, default: true --- opts.respect_gitignore (bool): if true will only add files that are not ignored by git --- opts.depth (int): depth on how deep the search should go, default: 1 --- opts.group_directories_first (bool): same as real ls --- opts.on_exit function(results): will be called at the end (required) +---@class PlenaryScandirLsAsyncOpts: PlenaryScandirLsOpts +---@field on_exit? fun(results: string[], sections: PlenaryScandirLsSection[][]): nil called at the end (required) + +---m.ls_async +---List directory contents. Will always apply --long option. Use scan_dir for without --long +---* param path: string +--- string has to be a valid path +---* param opts: table to change behavior +--- opts.hidden (bool): if true hidden files will be added +--- opts.add_dirs (bool): if true dirs will also be added to the results, default: true +--- opts.respect_gitignore (bool): if true will only add files that are not ignored by git +--- opts.depth (int): depth on how deep the search should go, default: 1 +--- opts.group_directories_first (bool): same as real ls +--- opts.on_exit function(results, sections): will be called at the end (required) +---@param path string +---@param opts? PlenaryScandirLsAsyncOpts m.ls_async = function(path, opts) opts = opts or {} opts.depth = opts.depth or 1 @@ -614,7 +731,7 @@ m.ls_async = function(path, opts) end end - m.scan_dir_async(path, opts_copy) + m.scan_dir_async(path, opts_copy --[[@as PlenaryScandirAsyncOpts]]) end return m diff --git a/lua/plenary/strings.lua b/lua/plenary/strings.lua index 3e8e5bc1a..eb6cfdddc 100644 --- a/lua/plenary/strings.lua +++ b/lua/plenary/strings.lua @@ -1,8 +1,13 @@ local path = require("plenary.path").path +---@class PlenaryStrings local M = {} +---@type fun(str: string, col?: integer): integer M.strdisplaywidth = (function() + ---@param str string + ---@param col? integer + ---@return integer local fallback = function(str, col) str = tostring(str) if vim.in_fast_event() then @@ -18,6 +23,9 @@ M.strdisplaywidth = (function() int linetabsize_col(int startcol, char_u *s); ]] + ---@param str string + ---@param col? integer + ---@return integer local ffi_func = function(str, col) str = tostring(str) local startcol = col or 0 @@ -37,7 +45,13 @@ M.strdisplaywidth = (function() end end)() +--- TODO: deal with the 4th param: `skipcc` +---@type fun(str: string, start: integer, len?: integer): string M.strcharpart = (function() + ---@param str string + ---@param nchar integer + ---@param charlen? integer + ---@return string local fallback = function(str, nchar, charlen) if vim.in_fast_event() then return str:sub(nchar + 1, charlen) @@ -52,6 +66,8 @@ M.strcharpart = (function() int utf_ptr2len(const char_u *const p); ]] + ---@param str string + ---@return integer local function utf_ptr2len(str) local c_str = ffi.new("char[?]", #str + 1) ffi.copy(c_str, str) @@ -63,6 +79,10 @@ M.strcharpart = (function() return fallback end + ---@param str string + ---@param nchar integer + ---@param charlen? integer + ---@return string return function(str, nchar, charlen) local nbyte = 0 if nchar > 0 then @@ -108,6 +128,11 @@ M.strcharpart = (function() end end)() +---@param str string +---@param len integer +---@param dots string +---@param direction -1|1 +---@return string local truncate = function(str, len, dots, direction) if M.strdisplaywidth(str) <= len then return str @@ -116,6 +141,10 @@ local truncate = function(str, len, dots, direction) local current = 0 local result = "" local len_of_dots = M.strdisplaywidth(dots) + ---@param a string + ---@param b string + ---@param dir -1|1 + ---@return string local concat = function(a, b, dir) if dir > 0 then return a .. b @@ -136,6 +165,11 @@ local truncate = function(str, len, dots, direction) return result end +---@param str string +---@param len integer +---@param dots? string default: `"…"` +---@param direction? -1|1 default: `1` +---@return string M.truncate = function(str, len, dots, direction) str = tostring(str) -- We need to make sure its an actually a string and not a number dots = dots or "…" @@ -154,18 +188,28 @@ M.truncate = function(str, len, dots, direction) end end +---@param string string +---@param width integer +---@param right_justify? boolean +---@return string M.align_str = function(string, width, right_justify) local str_len = M.strdisplaywidth(string) return right_justify and string.rep(" ", width - str_len) .. string or string .. string.rep(" ", width - str_len) end +---@param str string +---@param leave_indent? integer +---@return string M.dedent = function(str, leave_indent) -- Check each line and detect the minimum indent. + ---@type integer local indent + ---@type { line: string, chars?: integer, width?: integer }[] local info = {} for line in str:gmatch "[^\n]*\n?" do -- It matches '' for the last line. if line ~= "" then + ---@type integer, integer local chars, width local line_indent = line:match "^[ \t]+" if line_indent then @@ -184,6 +228,7 @@ M.dedent = function(str, leave_indent) -- Build up the result leave_indent = leave_indent or 0 + ---@type string[] local result = {} for _, i in ipairs(info) do local line diff --git a/lua/plenary/tbl.lua b/lua/plenary/tbl.lua index 276e9abe8..cc891283c 100644 --- a/lua/plenary/tbl.lua +++ b/lua/plenary/tbl.lua @@ -1,5 +1,10 @@ +---@class PlenaryTbl local tbl = {} +---@generic T +---@param original? table +---@param defaults T +---@return T function tbl.apply_defaults(original, defaults) if original == nil then original = {} @@ -16,10 +21,16 @@ function tbl.apply_defaults(original, defaults) return original end +---@param ... any +---@return table function tbl.pack(...) return { n = select("#", ...), ... } end +---@param t table +---@param i? integer +---@param j? integer +---@return ... function tbl.unpack(t, i, j) return unpack(t, i or 1, j or t.n or #t) end diff --git a/lua/plenary/window/border.lua b/lua/plenary/window/border.lua index a454b7f9c..c9b9bae2e 100644 --- a/lua/plenary/window/border.lua +++ b/lua/plenary/window/border.lua @@ -1,9 +1,42 @@ local strings = require "plenary.strings" +---@class PlenaryWindowBorder +---@field bufnr integer +---@field content_win_id integer +---@field content_win_options vim.api.keyset.win_config +---@field contents string[] +---@field win_id integer +---@field private _border_win_options PlenaryWindowBorderBorderOptions local Border = {} +---@class PlenaryWindowBorderBorderOptions +---@field border_thickness PlenaryWindowBorderBorderThickness +---@field topleft string +---@field topright string +---@field top string +---@field left string +---@field right string +---@field botleft string +---@field botright string +---@field bot string +---@field focusable? boolean +---@field highlight? string +---@field title? string|PlenaryWindowBorderTitles +---@field titlehighlight? string + +---@class PlenaryWindowBorderBorderThickness +---@field top integer +---@field right integer +---@field bot integer +---@field left integer + +---@alias PlenaryWindowBorderPos "NW"|"N"|"NE"|"SW"|"S"|"SE" +---@alias PlenaryWindowBorderRanges { [1]: integer, [2]: integer, [3]: integer? }[] +---@alias PlenaryWindowBorderTitles { text: string, pos: PlenaryWindowBorderPos }[] + Border.__index = Border +---@type PlenaryWindowBorderBorderThickness Border._default_thickness = { top = 1, right = 1, @@ -11,6 +44,10 @@ Border._default_thickness = { left = 1, } +---@param title_pos string +---@param title_len integer +---@param total_width integer +---@return integer local calc_left_start = function(title_pos, title_len, total_width) if string.find(title_pos, "W") then return 0 @@ -21,6 +58,15 @@ local calc_left_start = function(title_pos, title_len, total_width) end end +---comment +---@param title string +---@param pos PlenaryWindowBorderPos +---@param width integer +---@param left_char string +---@param mid_char string +---@param right_char string +---@return string horizontal_line +---@return PlenaryWindowBorderRanges ranges local create_horizontal_line = function(title, pos, width, left_char, mid_char, right_char) local title_len if title == "" then @@ -46,6 +92,7 @@ local create_horizontal_line = function(title, pos, width, left_char, mid_char, string.rep(mid_char, width - title_len - left_start), right_char ) + ---@type PlenaryWindowBorderRanges local ranges = {} if title_len ~= 0 then -- Need to calculate again due to multi-byte characters @@ -55,6 +102,11 @@ local create_horizontal_line = function(title, pos, width, left_char, mid_char, return horizontal_line, ranges end +---@param content_win_id integer +---@param content_win_options vim.api.keyset.win_config +---@param border_win_options PlenaryWindowBorderBorderOptions +---@return string[] border_lines +---@return PlenaryWindowBorderRanges ranges function Border._create_lines(content_win_id, content_win_options, border_win_options) local content_pos = vim.api.nvim_win_get_position(content_win_id) local content_height = vim.api.nvim_win_get_height(content_win_id) @@ -71,7 +123,9 @@ function Border._create_lines(content_win_id, content_win_options, border_win_op border_win_options.border_thickness.left = left_enabled and 1 or 0 border_win_options.border_thickness.right = right_enabled and 1 or 0 + ---@type string[] local border_lines = {} + ---@type PlenaryWindowBorderRanges local ranges = {} -- border_win_options.title should have be a list with entries of the @@ -79,7 +133,7 @@ function Border._create_lines(content_win_id, content_win_options, border_win_op -- pos can take values in { "NW", "N", "NE", "SW", "S", "SE" } local titles = type(border_win_options.title) == "string" and { { pos = "N", text = border_win_options.title } } or border_win_options.title - or {} + or {} --[[@as PlenaryWindowBorderTitles]] local topline = nil local topleft = (left_enabled and border_win_options.topleft) or "" @@ -164,6 +218,9 @@ function Border._create_lines(content_win_id, content_win_options, border_win_op return border_lines, ranges end +---@param bufnr integer +---@param ranges? PlenaryWindowBorderRanges +---@param hl? string local set_title_highlights = function(bufnr, ranges, hl) -- Check if both `hl` and `ranges` are provided, and `ranges` is not the empty table. if hl and ranges and next(ranges) then @@ -173,6 +230,8 @@ local set_title_highlights = function(bufnr, ranges, hl) end end +---@param new_title string +---@param pos string function Border:change_title(new_title, pos) if self._border_win_options.title == new_title then return @@ -195,6 +254,9 @@ end -- Updates characters for border lines, and returns nvim_win_config -- (generally used in conjunction with `move` or `new`) +---@param content_win_options vim.api.keyset.win_config +---@param border_win_options PlenaryWindowBorderBorderOptions +---@return vim.api.keyset.win_config function Border:__align_calc_config(content_win_options, border_win_options) border_win_options = vim.tbl_deep_extend("keep", border_win_options, { border_thickness = Border._default_thickness, @@ -239,6 +301,8 @@ end -- Sets the size and position of the given Border. -- Can be used to create a new window (with `create_window = true`) -- or change an existing one +---@param content_win_options vim.api.keyset.win_config +---@param border_win_options PlenaryWindowBorderBorderOptions function Border:move(content_win_options, border_win_options) -- Update lines in border buffer, and get config for border window local nvim_win_config = self:__align_calc_config(content_win_options, border_win_options) @@ -249,6 +313,11 @@ function Border:move(content_win_options, border_win_options) set_title_highlights(self.bufnr, self.title_ranges, self._border_win_options.titlehighlight) end +---@param content_bufnr integer +---@param content_win_id integer +---@param content_win_options vim.api.keyset.win_config +---@param border_win_options PlenaryWindowBorderBorderOptions +---@return PlenaryWindowBorder function Border:new(content_bufnr, content_win_id, content_win_options, border_win_options) assert(type(content_win_id) == "number", "Must supply a valid win_id. It's possible you forgot to call with ':'") diff --git a/lua/plenary/window/float.lua b/lua/plenary/window/float.lua index 86e5c046a..0151e98a8 100644 --- a/lua/plenary/window/float.lua +++ b/lua/plenary/window/float.lua @@ -3,6 +3,7 @@ local tbl = require "plenary.tbl" _AssociatedBufs = {} +---@param bufnr integer local clear_buf_on_leave = function(bufnr) vim.cmd( string.format( @@ -13,13 +14,22 @@ local clear_buf_on_leave = function(bufnr) ) end +---@class PlenaryWindowFloat +---@field default_options PlenaryWindowFloatOptions local win_float = {} +---@class PlenaryWindowFloatOptions +---@field bufnr? integer +---@field percentage float +---@field winblend float + win_float.default_options = { winblend = 15, percentage = 0.9, } +---@param options? PlenaryWindowFloatOptions +---@return vim.api.keyset.win_config function win_float.default_opts(options) options = tbl.apply_defaults(options, win_float.default_options) @@ -41,6 +51,8 @@ function win_float.default_opts(options) return opts end +---@param options? PlenaryWindowFloatOptions +---@return { bufnr: integer, win_id: integer } function win_float.centered(options) options = tbl.apply_defaults(options, win_float.default_options) @@ -60,6 +72,9 @@ function win_float.centered(options) } end +---@param top_text string[] +---@param options? PlenaryWindowFloatOptions +---@return { bufnr: integer, win_id: integer, minor_bufnr: integer, minor_win_id: integer } function win_float.centered_with_top_win(top_text, options) options = tbl.apply_defaults(options, win_float.default_options) @@ -135,6 +150,11 @@ end -- If table, first index should be start, second_index should be end --@param win_opts Table --@param border_opts Table +---@param col_range integer +---@param row_range integer +---@param win_opts? PlenaryWindowFloatOptions +---@param border_opts? PlenaryWindowBorderBorderOptions +---@return table function win_float.percentage_range_window(col_range, row_range, win_opts, border_opts) win_opts = tbl.apply_defaults(win_opts, win_float.default_options) @@ -195,6 +215,7 @@ function win_float.percentage_range_window(col_range, row_range, win_opts, borde } end +---@param bufnr integer function win_float.clear(bufnr) if _AssociatedBufs[bufnr] == nil then return diff --git a/lua/plenary/window/init.lua b/lua/plenary/window/init.lua index e5b85e159..2fb31129a 100644 --- a/lua/plenary/window/init.lua +++ b/lua/plenary/window/init.lua @@ -1,5 +1,8 @@ +---@class PlenaryWindow local window = {} +---@param win_id integer +---@param force? boolean window.try_close = function(win_id, force) if force == nil then force = true @@ -8,6 +11,8 @@ window.try_close = function(win_id, force) pcall(vim.api.nvim_win_close, win_id, force) end +---@param parent_win_id integer +---@param child_win_id integer window.close_related_win = function(parent_win_id, child_win_id) window.try_close(parent_win_id, true) window.try_close(child_win_id, true) diff --git a/tests/plenary/curl_spec.lua b/tests/plenary/curl_spec.lua index c4ba89aab..3c40757cf 100644 --- a/tests/plenary/curl_spec.lua +++ b/tests/plenary/curl_spec.lua @@ -84,7 +84,7 @@ describe("CURL Wrapper:", function() return done end) - eq(403, res.status, "It should return 403") + eq(403, res and res.status, "It should return 403") assert(not succ, "It should fail") vim.fn.delete(loc)