Skip to content
11 changes: 3 additions & 8 deletions lua/esqueleto/autocmd.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -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
Expand Down
46 changes: 29 additions & 17 deletions lua/esqueleto/config.lua
Original file line number Diff line number Diff line change
@@ -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" },
Expand All @@ -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
Expand Down
25 changes: 21 additions & 4 deletions lua/esqueleto/init.lua
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
15 changes: 15 additions & 0 deletions lua/esqueleto/selectors/builtin.lua
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions lua/esqueleto/selectors/telescope.lua
Original file line number Diff line number Diff line change
@@ -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
55 changes: 32 additions & 23 deletions lua/esqueleto/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -158,32 +163,35 @@ 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
Comment thread
cvigilv marked this conversation as resolved.
-- 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()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think only one breaking chang at that place, if it work correct, then it must work correct either errors exists at another functions

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this PR, I get the following error:

Error detected while processing BufNewFile Autocommands for "*":
Error executing lua callback: .../latest/nvim-macos-arm64/share/nvim/runtime/filetype.lua:36: BufNewFile Autocommands for "*"..FileType Autocommands for "*"..FileType Autocommands for "*"..FileType Autocomman
ds for "*"..FileType Autocommands for "*"..FileType Autocommands for "*"..FileType Autocommands for "*"..FileType Autocommands for "*"..FileType Autocommands for "*"..FileType Autocommands for "*": Vim(append
):Error executing lua callback: ...s/carlosvigil/git/esqueleto.nvim/lua/esqueleto/utils.lua:182: ...queleto_dev/lazy/plenary.nvim/lua/plenary/popup/init.lua:122: Vim:E218: Autocommand nesting too deep
stack traceback:
        [builtin#36]: at 0x01008f59f0
        ...s/carlosvigil/git/esqueleto.nvim/lua/esqueleto/utils.lua:182: in function 'selecttemplate'
        ...s/carlosvigil/git/esqueleto.nvim/lua/esqueleto/utils.lua:208: in function 'inserttemplate'
        ...carlosvigil/git/esqueleto.nvim/lua/esqueleto/autocmd.lua:19: in function <...carlosvigil/git/esqueleto.nvim/lua/esqueleto/autocmd.lua:15>
        [builtin#36]: at 0x01008f59f0
        ...s/carlosvigil/git/esqueleto.nvim/lua/esqueleto/utils.lua:182: in function 'selecttemplate'
        ...s/carlosvigil/git/esqueleto.nvim/lua/esqueleto/utils.lua:208: in function 'inserttemplate'
        ...carlosvigil/git/esqueleto.nvim/lua/esqueleto/autocmd.lua:19: in function <...carlosvigil/git/esqueleto.nvim/lua/esqueleto/autocmd.lua:15>
        [builtin#36]: at 0x01008f59f0
        ...s/carlosvigil/git/esqueleto.nvim/lua/esqueleto/utils.lua:182: in function 'selecttemplate'
        ...s/carlosvigil/git/esqueleto.nvim/lua/esqueleto/utils.lua:208: in function 'inserttemplate'
        ...
        [builtin#36]: at 0x01008f59f0
        ...s/carlosvigil/git/esqueleto.nvim/lua/esqueleto/utils.lua:182: in function 'selecttemplate'
        ...s/carlosvigil/git/esqueleto.nvim/lua/esqueleto/utils.lua:208: in function 'inserttemplate'
        ...carlosvigil/git/esqueleto.nvim/lua/esqueleto/autocmd.lua:19: in function <...carlosvigil/git/esqueleto.nvim/lua/esqueleto/autocmd.lua:15>
        [C]: in function 'nvim_cmd'
        .../latest/nvim-macos-arm64/share/nvim/runtime/filetype.lua:36: in function <.../latest/nvim-macos-arm64/share/nvim/runtime/filetype.lua:35>
        [C]: in function 'pcall'
        vim/shared.lua: in function <vim/shared.lua:0>
        [C]: in function '_with'
        .../latest/nvim-macos-arm64/share/nvim/runtime/filetype.lua:35: in function <.../latest/nvim-macos-arm64/share/nvim/runtime/filetype.lua:10>
stack traceback:
        [C]: in function '_with'
        .../latest/nvim-macos-arm64/share/nvim/runtime/filetype.lua:35: in function <.../latest/nvim-macos-arm64/share/nvim/runtime/filetype.lua:10>

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")
local filename = vim.fn.expand("%:t")
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
Expand All @@ -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)
Expand Down
22 changes: 13 additions & 9 deletions lua/esqueleto/wildcards.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)()
Expand All @@ -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

Expand Down