diff --git a/lua/esqueleto/autocmd.lua b/lua/esqueleto/autocmd.lua index 37dd293..efbb966 100644 --- a/lua/esqueleto/autocmd.lua +++ b/lua/esqueleto/autocmd.lua @@ -3,7 +3,7 @@ local utils = require("esqueleto.utils") local M = {} --- Create autocommands for `esqueleto.nvim` ----@param opts table Plugin configuration table +---@param opts Esqueleto.Config Plugin configuration table M.createautocmd = function(opts) -- create autocommands for skeleton insertion local group = vim.api.nvim_create_augroup("esqueleto", { clear = true }) @@ -13,15 +13,10 @@ M.createautocmd = function(opts) desc = "esqueleto.nvim :: Insert template", pattern = opts.patterns, callback = function() - if vim.bo.buftype == "nofile" then - return nil - end - + if vim.bo.buftype == "nofile" then return nil end local filepath = vim.fn.expand("%") local emptyfile = vim.fn.getfsize(filepath) < 4 - if emptyfile then - utils.inserttemplate(opts) - end + if emptyfile then utils.inserttemplate(opts) end end, }) end diff --git a/lua/esqueleto/config.lua b/lua/esqueleto/config.lua index b9f4697..8a4e0f9 100644 --- a/lua/esqueleto/config.lua +++ b/lua/esqueleto/config.lua @@ -1,7 +1,8 @@ +local selector = require("esqueleto.selectors.builtin") local utils = require("esqueleto.utils") - local M = {} +---@type Esqueleto.Config M.default_config = { autouse = true, directories = { vim.fn.stdpath("config") .. "/skeletons" }, @@ -17,43 +18,54 @@ M.default_config = { ["filetype"] = function() return vim.bo.filetype end, -- Date and time - ["date"] = function() return os.date("%Y%m%d", os.time()) end, - ["year"] = function() return os.date("%Y", os.time()) end, - ["month"] = function() return os.date("%m", os.time()) end, - ["day"] = function() return os.date("%d", os.time()) end, - ["time"] = function() return os.date("%T", os.time()) end, + ["date"] = function() + return os.date("%Y%m%d", os.time()) --[[@as string]] + end, + ["year"] = function() + return os.date("%Y", os.time()) --[[@as string]] + end, + ["month"] = function() + return os.date("%m", os.time()) --[[@as string]] + end, + ["day"] = function() + return os.date("%d", os.time()) --[[@as string]] + end, + ["time"] = function() + return os.date("%T", os.time()) --[[@as string]] + end, -- System ["host"] = utils.capture("hostname", false), - ["user"] = os.getenv("USER"), + ["user"] = os.getenv("USER") or "USER", -- Github ["gh-email"] = utils.capture("git config user.email", false), ["gh-user"] = utils.capture("git config user.name", false), }, }, + selector = selector, advanced = { ignored = {}, ignore_os_files = true, - } + }, } ---- Update default configuration table by merging with user's configuration table ----@param config table user configuration table ----@return table +---Update default configuration table by merging with user's configuration table +---@param config Esqueleto.Config user configuration table +---@return Esqueleto.Config M.updateconfig = function(config) vim.validate({ config = { config, "table", true } }) config = vim.tbl_deep_extend("force", M.default_config, config or {}) -- Validate setup vim.validate({ - autouse = { config.autouse, 'boolean' }, - directories = { config.directories, 'table' }, - patterns = { config.patterns, 'table' }, + autouse = { config.autouse, "boolean" }, + directories = { config.directories, "table" }, + patterns = { config.patterns, "table" }, wildcards = { config.wildcards, "table" }, - advanced = { config.advanced, 'table' }, - ["advanced.ignored"] = { config.advanced.ignored, { 'table', 'function' } }, - ["advanced.ignore_os_files"] = { config.advanced.ignore_os_files, 'boolean' }, + advanced = { config.advanced, "table" }, + ["advanced.ignored"] = { config.advanced.ignored, { "table", "function" } }, + ["advanced.ignore_os_files"] = { config.advanced.ignore_os_files, "boolean" }, }) return config diff --git a/lua/esqueleto/init.lua b/lua/esqueleto/init.lua index df6c2af..801fc19 100644 --- a/lua/esqueleto/init.lua +++ b/lua/esqueleto/init.lua @@ -1,13 +1,30 @@ _G.esqueleto_inserted = {} -local autocmd = require('esqueleto.autocmd') -local excmd = require('esqueleto.excmd') -local config = require('esqueleto.config') +local autocmd = require("esqueleto.autocmd") +local config = require("esqueleto.config") +local excmd = require("esqueleto.excmd") + +---@alias Wildcard { [string] : string | fun():string } +---@class Wildcards +---@field lookup Wildcard +---@field expand boolean Replace ${} with an instance of a wildcards array? + +---@class Esqueleto.Advanced +---@field ignored string[] | fun(file:string):boolean Array of glob file filters +---@field ignore_os_files boolean To ignore OS files? + +---@class Esqueleto.Config +---@field directories string[] The list of paths in which the search will be performed +---@field patterns string[] See: [vim.api.nvim_create_autocmd](lua://vim.api.nvim_create_autocmd) +---@field autouse boolean Auto-use a template? +---@field selector fun( templatenames: string[] ):string? Function that choose some of template +---@field wildcards Wildcards +---@field advanced Esqueleto.Advanced local M = {} --- Setup `esqueleto.nvim` ----@param opts table User configuration table +---@param opts Esqueleto.Config User configuration table M.setup = function(opts) -- update defaults opts = config.updateconfig(opts) diff --git a/lua/esqueleto/selectors/builtin.lua b/lua/esqueleto/selectors/builtin.lua new file mode 100644 index 0000000..7b1d181 --- /dev/null +++ b/lua/esqueleto/selectors/builtin.lua @@ -0,0 +1,15 @@ +return function(templates) + -- Sync call ui.select + -- See: https://github.com/mfussenegger/nvim-dap/blob/66d33b7585b42b7eac20559f1551524287ded353/lua/dap/ui.lua#L55 + local co = coroutine.running() + local choicer = function(choice) + if not choice then + vim.notify("[esqueleto] No template selected, leaving buffer empty", vim.log.levels.INFO) + end + coroutine.resume(co, choice) + end + -- I don't know reason to use that + choicer = vim.schedule_wrap(choicer) + vim.ui.select(vim.tbl_keys(templates), { prompt = "Select skeleton to use:" }, choicer) + return coroutine.yield() +end diff --git a/lua/esqueleto/selectors/telescope.lua b/lua/esqueleto/selectors/telescope.lua new file mode 100644 index 0000000..c428e0d --- /dev/null +++ b/lua/esqueleto/selectors/telescope.lua @@ -0,0 +1,34 @@ +return function(templates) + local co = coroutine.running() + local pickers = require("telescope.pickers") + local finders = require("telescope.finders") + local actions = require("telescope.actions") + local action_state = require("telescope.actions.state") + pickers + .new({}, { + prompt_title = "Templates", + previewer = _TelescopeConfigurationValues.file_previewer({}), + finder = finders.new_table({ + results = vim.tbl_keys(templates), + entry_maker = function(entry) + return { + value = entry, + display = entry, + ordinal = entry, + filename = templates[entry], + } + end, + }), + sorter = _TelescopeConfigurationValues.generic_sorter({}), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + coroutine.resume(co, selection.value) + end) + return true + end, + }) + :find() + return coroutine.yield() +end diff --git a/lua/esqueleto/utils.lua b/lua/esqueleto/utils.lua index 4d408c3..481a60b 100644 --- a/lua/esqueleto/utils.lua +++ b/lua/esqueleto/utils.lua @@ -31,7 +31,7 @@ end --- Write template contents to current buffer ---@param file string Template file path ----@param opts table Plugin configuration table +---@param opts Esqueleto.Config Plugin configuration table M.writetemplate = function(file, opts) if file == nil then -- Do an early return if no files are specified @@ -41,7 +41,7 @@ M.writetemplate = function(file, opts) local handler, message = io.open(file, "r") if handler == nil then -- Print error message and abort if no file handlers are created - vim.notify(message, vim.log.levels.ERROR) + vim.notify(message --[[@as string]], vim.log.levels.ERROR) return end @@ -69,17 +69,21 @@ M.writetemplate = function(file, opts) end end --- List ignored files under a directory, given a list of glob patterns +---List ignored files under a directory, given a list of glob patterns +---@param dir string +---@param ignored_patterns string[] +---@return string[] local listignored = function(dir, ignored_patterns) - return vim.tbl_flatten( - vim.tbl_map( - function(patterns) return vim.fn.globpath(dir, patterns, true, true, true) end, - ignored_patterns - ) - ) + return vim + .iter(ignored_patterns) + :map(function(patterns) return vim.fn.globpath(dir, patterns, true, true, true) end) + :flatten() + :totable() end --- Returns a ignore checker +---Returns a ignore checker +---@param opts Esqueleto.Config +---@return fun(filepath:string):boolean local getignorechecker = function(opts) local os_ignore_pats = opts.advanced.ignore_os_files and require("esqueleto.constants").ignored_os_patterns @@ -104,12 +108,13 @@ end --- Get available templates for current buffer ---@param pattern string Pattern to use to find templates ----@param opts table Plugin configuration table ----@return table templates Available templates for current buffer +---@param opts Esqueleto.Config Plugin configuration table +---@return {[string]:string} templates Available templates for current buffer M.gettemplates = function(pattern, opts) + ---@type {[string]:string} local templates = {} local isignored = getignorechecker(opts) - + ---@type string[] local alldirectories = vim.tbl_map( function(f) return vim.fn.fnamemodify(f, ":p") end, opts.directories @@ -142,8 +147,8 @@ M.gettemplates = function(pattern, opts) end --- Select template to insert on current buffer ----@param templates table Available template table ----@param opts table Plugin configuration table +---@param templates {[string]:string} Available template table +---@param opts Esqueleto.Config Plugin configuration table M.selecttemplate = function(templates, opts) -- Check if templates exist if vim.tbl_isempty(templates) then @@ -158,24 +163,27 @@ M.selecttemplate = function(templates, opts) local templatenames = vim.tbl_keys(templates) table.sort(templatenames, function(a, b) return a:lower() < b:lower() end) + local uv = vim.loop or vim.uv -- If only one template, write and return early if #templatenames == 1 and opts.autouse then - M.writetemplate(vim.loop.fs_realpath(templates[templatenames[1]]), opts) + M.writetemplate(uv.fs_realpath(templates[templatenames[1]])--[[@as string]], opts) return nil end -- Select template - vim.ui.select(templatenames, { prompt = "Select skeleton to use:" }, function(choice) + + -- wrap to coroutine to have possibility to async function + ---@see esqueleto.selectors.builtin + coroutine.wrap(function() + local choice = opts.selector(templates) if templates[choice] then - M.writetemplate(vim.loop.fs_realpath(templates[choice]), opts) - else - vim.notify("[esqueleto] No template selected, leaving buffer empty", vim.log.levels.INFO) + M.writetemplate(uv.fs_realpath(templates[choice]) --[[@as string]], opts) end - end) + end)() end --- Insert template on current buffer ----@param opts table Plugin configuration table +---@param opts Esqueleto.Config Plugin configuration table M.inserttemplate = function(opts) -- Get pattern alternatives for current file local filepath = vim.fn.expand("%:p") @@ -183,7 +191,7 @@ M.inserttemplate = function(opts) local filetype = vim.bo.filetype -- Identify if pattern matches user configuration - local pattern = nil + local pattern = "" if not _G.esqueleto_inserted[filepath] then -- match either filename or extension. Filename has priority if vim.tbl_contains(opts.patterns, filename) then @@ -194,6 +202,7 @@ M.inserttemplate = function(opts) -- Get templates for selected pattern local templates = M.gettemplates(pattern, opts) + -- Identify if pattern matches user configuration -- Pop-up selection UI M.selecttemplate(templates, opts) diff --git a/lua/esqueleto/wildcards.lua b/lua/esqueleto/wildcards.lua index e6d917a..52f6623 100644 --- a/lua/esqueleto/wildcards.lua +++ b/lua/esqueleto/wildcards.lua @@ -2,17 +2,23 @@ local M = {} --- Parse template contents in order to expand wildcards ---@param str string String to parse ----@param lookup table Wildcards lookup table ----@return table parsed_str Table containing all lines with wildcards expanded table ----@return table cursor_pos Row-column position tuple of the last cursor wildcard found. +---@param lookup Wildcard Wildcards lookup table +---@return string[] parsed_str Table containing all lines with wildcards expanded table +---@return nil | [integer, integer] cursor_pos Row-column position tuple of the last cursor wildcard found. M.parse = function(str, lookup) + ---@type string[] local parsedstr = {} for _, l in ipairs(vim.split(str, "\n", { plain = true })) do for wildcard in l:gmatch("${([^{,^}]+)}") do - local expansion = nil - + ---@type string + local expansion if vim.tbl_contains(vim.tbl_keys(lookup), wildcard) then - expansion = lookup[wildcard] + local wild = lookup[wildcard] + if type(wild) == "function" then + expansion = wild() + else + expansion = wild + end elseif string.find(wildcard, "lua:") then local cmdstr = l:gsub(".*${lua:([^{,^}]+)}.*", "%1") local cmdout = load("return " .. cmdstr)() @@ -30,9 +36,7 @@ M.parse = function(str, lookup) local cursor_pos = nil for row, l in ipairs(parsedstr) do local col, _ = string.find(l, "${cursor}") - if col ~= nil then - cursor_pos = {row, col} - end + if col ~= nil then cursor_pos = { row, col } end parsedstr[row] = parsedstr[row]:gsub("${cursor}", "") end