|
| 1 | +local wildcards = require("esqueleto.helpers.wildcards") |
| 2 | + |
| 3 | +local M = {} |
| 4 | + |
| 5 | +--- Capture output of command |
| 6 | +---@param cmd string Command to run |
| 7 | +---@param raw boolean Whether the function returns the raw string |
| 8 | +---@return string output Command standard output |
| 9 | +M.capture = function(cmd, raw) |
| 10 | + local f = assert(io.popen(cmd, "r")) |
| 11 | + local s = assert(f:read("*a")) |
| 12 | + f:close() |
| 13 | + if raw then return s end |
| 14 | + s = string.gsub(s, "^%s+", "") |
| 15 | + s = string.gsub(s, "%s+$", "") |
| 16 | + s = string.gsub(s, "[\n\r]+", "") |
| 17 | + return s |
| 18 | +end |
| 19 | + |
| 20 | +--- Map function over each entry in table |
| 21 | +---@param tbl table Table to map |
| 22 | +---@param f function Function to map |
| 23 | +---@return table mapped_tbl Function-mapped table |
| 24 | +M.map = function(tbl, f) |
| 25 | + local t = {} |
| 26 | + for k, v in pairs(tbl) do |
| 27 | + t[k] = f(v) |
| 28 | + end |
| 29 | + return t |
| 30 | +end |
| 31 | + |
| 32 | +--- Write template contents to current buffer |
| 33 | +---@param file string Template file path |
| 34 | +---@param opts Esqueleto.Config Plugin configuration table |
| 35 | +M.writetemplate = function(file, opts) |
| 36 | + if file == nil then |
| 37 | + -- Do an early return if no files are specified |
| 38 | + return |
| 39 | + end |
| 40 | + |
| 41 | + local uv = vim.uv or vim.loop |
| 42 | + ---@diagnostic disable-next-line: undefined-field |
| 43 | + local handler, message = io.open(uv.fs_realpath(file), "r") |
| 44 | + if handler == nil then |
| 45 | + -- Print error message and abort if no file handlers are created |
| 46 | + vim.notify(message --[[@as string]], vim.log.levels.ERROR) |
| 47 | + return |
| 48 | + end |
| 49 | + |
| 50 | + -- Read the file, convert EOL to LF and remove the new line at EOF |
| 51 | + local content = handler:read("*a"):gsub("\r\n?", "\n"):gsub("\n$", "") |
| 52 | + |
| 53 | + local lines, cursor_pos |
| 54 | + if opts.wildcards.expand then |
| 55 | + -- Place the contents of the file with the wildcards expanded |
| 56 | + lines, cursor_pos = wildcards.parse(content, opts.wildcards.lookup) |
| 57 | + else |
| 58 | + -- ... or place them directly |
| 59 | + lines = vim.split(content, "\n", { plain = true }) |
| 60 | + end |
| 61 | + |
| 62 | + -- Replace the buffer with the given lines |
| 63 | + vim.api.nvim_buf_set_lines(0, 0, -1, true, lines) |
| 64 | + |
| 65 | + if cursor_pos ~= nil then |
| 66 | + -- If a cursor wildcard was found, place the cursor there |
| 67 | + vim.api.nvim_win_set_cursor(0, cursor_pos) |
| 68 | + else |
| 69 | + -- If not, move the cursor to the last line |
| 70 | + vim.cmd("norm! G") |
| 71 | + end |
| 72 | +end |
| 73 | + |
| 74 | +-- List ignored files under a directory, given a list of glob patterns |
| 75 | +local listignored = function(dir, ignored_patterns) |
| 76 | + return vim |
| 77 | + .iter(ignored_patterns) |
| 78 | + :map(function(patterns) return vim.fn.globpath(dir, patterns, true, true, true) end) |
| 79 | +end |
| 80 | + |
| 81 | +-- Returns a ignore checker |
| 82 | +local getignorechecker = function(opts) |
| 83 | + local os_ignore_pats = opts.advanced.ignore_os_files |
| 84 | + and require("esqueleto.helpers.constants").ignored_os_patterns |
| 85 | + or {} |
| 86 | + local extra = opts.advanced.ignored |
| 87 | + local extra_ignore_pats, extra_ignore_func = (function() |
| 88 | + if type(extra) == "function" then |
| 89 | + return {}, extra |
| 90 | + else |
| 91 | + assert(type(extra) == "table") |
| 92 | + return extra, function(_) return false end |
| 93 | + end |
| 94 | + end)() |
| 95 | + |
| 96 | + return function(filepath) |
| 97 | + local dir = vim.fn.fnamemodify(filepath, ":p:h") |
| 98 | + return extra_ignore_func(dir) |
| 99 | + or vim.tbl_contains(listignored(dir, os_ignore_pats), filepath) |
| 100 | + or vim.tbl_contains(listignored(dir, extra_ignore_pats), filepath) |
| 101 | + end |
| 102 | +end |
| 103 | + |
| 104 | +--- Get available templates for current buffer |
| 105 | +---@param pattern string Pattern to use to find templates |
| 106 | +---@param opts Esqueleto.Config Plugin configuration table |
| 107 | +---@return table templates Available templates for current buffer |
| 108 | +M.gettemplates = function(pattern, opts) |
| 109 | + local templates = {} |
| 110 | + local isignored = getignorechecker(opts) |
| 111 | + |
| 112 | + local alldirectories = vim.tbl_map( |
| 113 | + function(f) return vim.fn.fnamemodify(f, ":p") end, |
| 114 | + opts.directories --[[@as table<string>]] |
| 115 | + ) |
| 116 | + |
| 117 | + -- Count directories that contain templates for pattern |
| 118 | + local ndirs = 0 |
| 119 | + for _, directory in pairs(alldirectories) do |
| 120 | + ndirs = ndirs + vim.fn.isdirectory(directory .. pattern .. "/") |
| 121 | + end |
| 122 | + |
| 123 | + -- Get templates for pattern |
| 124 | + for _, directory in ipairs(alldirectories) do |
| 125 | + local pattern_dir = directory .. pattern .. "/" |
| 126 | + local exists_dir = vim.fn.isdirectory(pattern_dir) == 1 |
| 127 | + if exists_dir then |
| 128 | + for basename in vim.fs.dir(pattern_dir) do |
| 129 | + local filepath = vim.fs.normalize(pattern_dir .. basename) |
| 130 | + -- Check if pattern is ignored |
| 131 | + if not isignored(filepath) then |
| 132 | + local name = vim.fs.basename(filepath) |
| 133 | + if ndirs > 1 then name = vim.fn.simplify(directory) .. " :: " .. name end |
| 134 | + templates[name] = filepath |
| 135 | + end |
| 136 | + end |
| 137 | + end |
| 138 | + end |
| 139 | + |
| 140 | + return templates |
| 141 | +end |
| 142 | + |
| 143 | +--- Select template to insert on current buffer |
| 144 | +---@param templates table Available template table |
| 145 | +---@param opts Esqueleto.Config Plugin configuration table |
| 146 | +M.selecttemplate = function(templates, opts) |
| 147 | + -- Check if templates exist |
| 148 | + if vim.tbl_isempty(templates) then |
| 149 | + vim.notify( |
| 150 | + "[WARNING] No templates found for this file! Pattern is known by `esqueleto` but could not find any template file", |
| 151 | + vim.log.levels.WARN |
| 152 | + ) |
| 153 | + return nil |
| 154 | + end |
| 155 | + |
| 156 | + -- Alphabetically sort template names for a more pleasing experience |
| 157 | + local templatenames = vim.tbl_keys(templates) |
| 158 | + table.sort(templatenames, function(a, b) return a:lower() < b:lower() end) |
| 159 | + |
| 160 | + -- If only one template, write and return early |
| 161 | + if #templatenames == 1 and opts.autouse then |
| 162 | + M.writetemplate(templates[templatenames[1]], opts) |
| 163 | + return nil |
| 164 | + end |
| 165 | + |
| 166 | + -- Select template |
| 167 | + vim.ui.select(templatenames, { prompt = "Select skeleton to use:" }, function(choice) |
| 168 | + if templates[choice] then |
| 169 | + ---@diagnostic disable-next-line: undefined-field |
| 170 | + M.writetemplate(vim.loop.fs_realpath(templates[choice]), opts) |
| 171 | + else |
| 172 | + vim.notify("[esqueleto] No template selected, leaving buffer empty", vim.log.levels.INFO) |
| 173 | + end |
| 174 | + end) |
| 175 | +end |
| 176 | + |
| 177 | +--- Insert template on current buffer |
| 178 | +---@param opts Esqueleto.Config Plugin configuration table |
| 179 | +M.inserttemplate = function(opts) |
| 180 | + -- Get pattern alternatives for current file |
| 181 | + local filepath = vim.fn.expand("%:p") |
| 182 | + local filename = vim.fn.expand("%:t") |
| 183 | + local filetype = vim.bo.filetype |
| 184 | + |
| 185 | + -- Identify if pattern matches user configuration |
| 186 | + local pattern |
| 187 | + if not _G.esqueleto_inserted[filepath] then |
| 188 | + -- match either filename or extension. Filename has priority |
| 189 | + if |
| 190 | + vim.tbl_contains(opts.patterns --[[@as table]], filename) |
| 191 | + then |
| 192 | + pattern = filename |
| 193 | + elseif |
| 194 | + vim.tbl_contains(opts.patterns --[[@as table]], filetype) |
| 195 | + then |
| 196 | + pattern = filetype |
| 197 | + end |
| 198 | + |
| 199 | + -- Get templates for selected pattern |
| 200 | + local templates = M.gettemplates(pattern, opts) |
| 201 | + |
| 202 | + -- Pop-up selection UI |
| 203 | + M.selecttemplate(templates, opts) |
| 204 | + _G.esqueleto_inserted[filepath] = true |
| 205 | + end |
| 206 | +end |
| 207 | + |
| 208 | +return M |
0 commit comments