Last active
April 16, 2025 18:18
-
-
Save WarrenBuffering/fba65ab9e79d639b121f772bc441a19a to your computer and use it in GitHub Desktop.
neovim template thing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useFetchStatus } from "hooks"; | |
import { ${SERVICE_NAME} } from "services"; | |
import type { | |
${PASCAL_SERVICE_NAME}Params, | |
${PASCAL_SERVICE_NAME}Data, | |
${PASCAL_SERVICE_NAME}Response, | |
} from "services"; | |
import type { StoreAction } from "types"; | |
/* ============================================================================ | |
= Helpers | |
============================================================================ */ | |
/* ============================================================================ | |
= Types | |
============================================================================ */ | |
type Params = ${PASCAL_SERVICE_NAME}Params; | |
type Response = ${PASCAL_SERVICE_NAME}Response; | |
/* ============================================================================ | |
= Action Hook | |
============================================================================ */ | |
export function use${NAME}(): StoreAction<Response, Params> { | |
const status = useFetchStatus(); | |
async function request(args: Params): Response { | |
status.start(); | |
const response = await ${SERVICE_NAME}(args); | |
if (response.data) { | |
status.succeed(); | |
// TODO: do stuff | |
} else { | |
status.fail(response.error); | |
} | |
return response; | |
} | |
return { | |
isFetching: status.isFetching, | |
isError: status.isError, | |
isSuccess: status.isSuccess, | |
isUnfetched: status.isUnfetched, | |
errorMessage: status.errorMessage, | |
errorCode: status.errorCode, | |
request, | |
reset: status.reset, | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local M = {} | |
function M.setup() | |
local function read_template_file(filepath) | |
local file = io.open(filepath, "r") | |
if not file then | |
error("Could not open template: " .. filepath) | |
end | |
local content = file:read("*all") | |
file:close() | |
return content | |
end | |
local function to_kebab_case(str) | |
if not str then | |
return "" | |
end | |
return str:gsub("(%u)", "-%1"):gsub("_", "-"):gsub("%s+", "-"):lower():gsub("^%-", "") | |
end | |
local function to_pascal_case(str) | |
if not str then | |
return "" | |
end | |
local has_mixed_case = str:match("[a-z][A-Z]") | |
if has_mixed_case then | |
return str:sub(1, 1):upper() .. str:sub(2) | |
else | |
str = str:gsub("[-_]", " ") | |
local result = string.gsub(str, "(%a)([%w_']*)", function(first, rest) | |
return first:upper() .. rest:lower() | |
end) | |
result = result:gsub("%s+", "") | |
return result | |
end | |
end | |
local function to_camel_case(str) | |
if not str then | |
return "" | |
end | |
local has_mixed_case = str:match("[a-z][A-Z]") | |
if has_mixed_case then | |
return str:sub(1, 1):lower() .. str:sub(2) | |
else | |
local pascal = to_pascal_case(str) | |
return pascal:sub(1, 1):lower() .. pascal:sub(2) | |
end | |
end | |
local template_dir = vim.fn.stdpath("config") .. "/lua/templates" | |
local template_definitions = { | |
["action-hook"] = { | |
pattern = function(name, _args) | |
return "use" .. name .. ".tsx" | |
end, | |
is_directory = false, | |
arguments = { | |
{ name = "s", description = "Service name (without 'Service' suffix)", default = "" }, | |
{ name = "service", description = "Service name (without 'Service' suffix)", default = "" }, | |
}, | |
files = { | |
{ | |
name = function(name, _args) | |
return "use" .. name .. ".tsx" | |
end, | |
template = template_dir .. "/action-hook/index.txt", | |
}, | |
}, | |
transformations = { | |
["SERVICE_NAME"] = function(args) | |
local service = args["service"] or args["s"] or "" | |
if service:sub(-7) == "Service" then | |
return service | |
else | |
return service .. "Service" | |
end | |
end, | |
["PASCAL_SERVICE_NAME"] = function(args) | |
local service = args["service"] or args["s"] or "" | |
if service:sub(-7) == "Service" then | |
service = service:sub(1, -8) | |
end | |
local has_mixed_case = service:match("[a-z][A-Z]") | |
if has_mixed_case then | |
return service:sub(1, 1):upper() .. service:sub(2) | |
else | |
return to_pascal_case(service) | |
end | |
end, | |
}, | |
}, | |
["component"] = { | |
pattern = "index.tsx", | |
is_directory = true, | |
arguments = { | |
{ name = "with-test", description = "Include test file", default = false }, | |
{ name = "with-storybook", description = "Include storybook file", default = false }, | |
}, | |
files = { | |
{ | |
name = "index.tsx", | |
template = template_dir .. "/react-component/index.txt", | |
}, | |
{ | |
name = "styles.css", | |
template = template_dir .. "/react-component/styles.txt", | |
}, | |
{ | |
name = function(name, args) | |
if args["with-test"] then | |
return name .. ".test.tsx" | |
end | |
return nil | |
end, | |
template = template_dir .. "/react-component/test.txt", | |
}, | |
{ | |
name = function(name, args) | |
if args["with-storybook"] then | |
return name .. ".stories.tsx" | |
end | |
return nil | |
end, | |
template = template_dir .. "/react-component/stories.txt", | |
}, | |
}, | |
}, | |
["native-component"] = { | |
pattern = "index.tsx", | |
is_directory = true, | |
arguments = {}, | |
files = { | |
{ | |
name = "index.tsx", | |
template = template_dir .. "/react-native-component/index.txt", | |
}, | |
}, | |
}, | |
["screen"] = { | |
pattern = "index.tsx", | |
is_directory = true, | |
arguments = { | |
{ name = "with-test", description = "Include test file", default = false }, | |
}, | |
files = { | |
{ | |
name = "index.tsx", | |
template = template_dir .. "/react-screen/index.txt", | |
}, | |
{ | |
name = "styles.css", | |
template = template_dir .. "/react-screen/styles.txt", | |
}, | |
{ | |
name = function(name, args) | |
if args["with-test"] then | |
return name .. ".test.tsx" | |
end | |
return nil | |
end, | |
template = template_dir .. "/react-screen/test.txt", | |
}, | |
}, | |
}, | |
["native-screen"] = { | |
pattern = "index.tsx", | |
is_directory = true, | |
arguments = {}, | |
files = { | |
{ | |
name = "index.tsx", | |
template = template_dir .. "/react-native-screen/index.txt", | |
}, | |
}, | |
}, | |
["store"] = { | |
pattern = function(name, _args) | |
return name .. "Store.ts" | |
end, | |
is_directory = false, | |
arguments = { | |
{ name = "with-actions", description = "Include actions file", default = false }, | |
{ name = "with-types", description = "Include types file", default = false }, | |
}, | |
files = { | |
{ | |
name = function(name, _args) | |
return name .. "Store.ts" | |
end, | |
template = template_dir .. "/store/index.txt", | |
}, | |
{ | |
name = function(name, args) | |
if args["with-actions"] then | |
return name .. "Actions.ts" | |
end | |
return nil | |
end, | |
template = template_dir .. "/store/actions.txt", | |
}, | |
{ | |
name = function(name, args) | |
if args["with-types"] then | |
return name .. "Types.ts" | |
end | |
return nil | |
end, | |
template = template_dir .. "/store/types.txt", | |
}, | |
}, | |
}, | |
["persisted-store"] = { | |
pattern = function(name, _args) | |
return name .. "Store.ts" | |
end, | |
is_directory = false, | |
arguments = {}, | |
files = { | |
{ | |
name = function(name, _args) | |
return name .. "Store.ts" | |
end, | |
template = template_dir .. "/persisted-store/index.txt", | |
}, | |
}, | |
}, | |
["persisted-store-rn"] = { | |
pattern = function(name, _args) | |
return name .. "Store.ts" | |
end, | |
is_directory = false, | |
arguments = {}, | |
files = { | |
{ | |
name = function(name, _args) | |
return name .. "Store.ts" | |
end, | |
template = template_dir .. "/persisted-store-rn/index.txt", | |
}, | |
}, | |
}, | |
} | |
local command_configurations = { | |
["CreateComponent"] = { | |
flags = { | |
["-r"] = "component", | |
["-rn"] = "native-component", | |
}, | |
default_flag = "-r", | |
}, | |
["CreateScreen"] = { | |
flags = { | |
["-r"] = "screen", | |
["-rn"] = "native-screen", | |
}, | |
default_flag = "-r", | |
}, | |
["CreateStore"] = { | |
flags = { | |
[""] = "store", | |
}, | |
default_flag = "", | |
}, | |
["CreatePersistedStore"] = { | |
flags = { | |
["-r"] = "persisted-store", | |
["-rn"] = "persisted-store-rn", | |
}, | |
default_flag = "-r", | |
}, | |
["CreateActionHook"] = { | |
flags = { | |
[""] = "action-hook", | |
}, | |
default_flag = "", | |
}, | |
} | |
local function evaluate_pattern(pattern, name, args) | |
if type(pattern) == "function" then | |
return pattern(name, args) | |
end | |
return pattern | |
end | |
local function apply_transformations(content, name, args, transformations) | |
local class_name = to_kebab_case(name) | |
content = content:gsub("${NAME}", name) | |
content = content:gsub("${CLASSNAME}", class_name) | |
if transformations then | |
for placeholder, transform_fn in pairs(transformations) do | |
local value = transform_fn(args) | |
content = content:gsub("${" .. placeholder .. "}", value) | |
end | |
end | |
for arg_name, arg_value in pairs(args) do | |
content = content:gsub("${" .. string.upper(arg_name) .. "}", tostring(arg_value)) | |
end | |
return content | |
end | |
local function normalize_arg_name(name) | |
if name == "service" then | |
return { "service", "s" } | |
elseif name == "s" then | |
return { "s", "service" } | |
end | |
return { name } | |
end | |
local function parse_custom_args(args_str, template_config) | |
local result = {} | |
for _, arg_def in ipairs(template_config.arguments or {}) do | |
result[arg_def.name] = arg_def.default | |
end | |
if args_str and args_str ~= "" then | |
local words = vim.split(args_str, " ", { trimempty = true }) | |
local i = 1 | |
while i <= #words do | |
local current = words[i] | |
local prefix = current:match("^%-+") | |
if prefix then | |
local name = current:sub(#prefix + 1) | |
local value | |
-- Check for name=value format | |
local name_value = vim.split(name, "=", { trimempty = true }) | |
if #name_value > 1 then | |
name = name_value[1] | |
value = name_value[2] | |
i = i + 1 | |
-- Check for -s value or --service value format | |
elseif i < #words and not words[i + 1]:match("^%-") then | |
value = words[i + 1] | |
i = i + 2 | |
-- Flag with no value | |
else | |
value = true | |
i = i + 1 | |
end | |
if value == "true" then | |
value = true | |
elseif value == "false" then | |
value = false | |
end | |
-- Special handling for -s and --service | |
if name == "s" or name == "service" then | |
result["s"] = value | |
result["service"] = value | |
else | |
-- Handle other arguments normally | |
for _, arg_def in ipairs(template_config.arguments or {}) do | |
if arg_def.name == name then | |
result[name] = value | |
break | |
end | |
end | |
end | |
else | |
i = i + 1 | |
end | |
end | |
end | |
return result | |
end | |
local function create_template(name, template_key, custom_args) | |
local config = template_definitions[template_key] | |
if not config then | |
vim.notify("Unknown template type: " .. template_key, vim.log.levels.ERROR) | |
return | |
end | |
local args = parse_custom_args(custom_args, config) | |
local cwd = vim.fn.expand("%:p:h") | |
local output_dir = config.is_directory and (cwd .. "/" .. name) or cwd | |
if config.is_directory then | |
vim.fn.mkdir(output_dir, "p") | |
end | |
local main_file_path = nil | |
local main_pattern = evaluate_pattern(config.pattern, name, args) | |
for _, file_config in ipairs(config.files) do | |
local filename = evaluate_pattern(file_config.name, name, args) | |
if filename then | |
local content = read_template_file(file_config.template) | |
content = apply_transformations(content, name, args, config.transformations) | |
local out_path = output_dir .. "/" .. filename | |
local out_file = io.open(out_path, "w") | |
if out_file then | |
out_file:write(content) | |
out_file:close() | |
if filename == main_pattern then | |
main_file_path = out_path | |
end | |
else | |
vim.notify("Failed to write file: " .. out_path, vim.log.levels.ERROR) | |
end | |
end | |
end | |
if main_file_path then | |
vim.cmd("edit " .. main_file_path) | |
end | |
vim.notify(template_key .. " '" .. name .. "' created!", vim.log.levels.INFO) | |
end | |
local function parse_args(args) | |
if not args or args == "" then | |
return nil, nil, nil | |
end | |
local parts = {} | |
local in_quotes = false | |
local current_part = "" | |
for i = 1, #args do | |
local char = args:sub(i, i) | |
if char == '"' then | |
in_quotes = not in_quotes | |
elseif char == " " and not in_quotes then | |
if current_part ~= "" then | |
table.insert(parts, current_part) | |
current_part = "" | |
end | |
else | |
current_part = current_part .. char | |
end | |
end | |
if current_part ~= "" then | |
table.insert(parts, current_part) | |
end | |
local name = parts[1] | |
local flag = nil | |
local custom_args_str = nil | |
local flag_index = nil | |
for i = 2, #parts do | |
if parts[i]:match("^%-r") or parts[i]:match("^%-rn") then | |
flag = parts[i] | |
flag_index = i | |
break | |
end | |
end | |
if not flag_index then | |
if #parts > 1 then | |
custom_args_str = table.concat(parts, " ", 2) | |
end | |
else | |
if flag_index > 2 then | |
local before_flag = {} | |
for i = 2, flag_index - 1 do | |
table.insert(before_flag, parts[i]) | |
end | |
custom_args_str = table.concat(before_flag, " ") | |
end | |
if flag_index < #parts then | |
local after_flag = {} | |
for i = flag_index + 1, #parts do | |
table.insert(after_flag, parts[i]) | |
end | |
if custom_args_str and custom_args_str ~= "" then | |
custom_args_str = custom_args_str .. " " .. table.concat(after_flag, " ") | |
else | |
custom_args_str = table.concat(after_flag, " ") | |
end | |
end | |
end | |
return name, flag, custom_args_str | |
end | |
for command, config in pairs(command_configurations) do | |
pcall(vim.api.nvim_del_user_command, command) | |
vim.api.nvim_create_user_command(command, function(opts) | |
local raw_args = opts.args or "" | |
local name, flag, custom_args = parse_args(raw_args) | |
if not name or name == "" then | |
vim.notify("Missing name for :" .. command, vim.log.levels.ERROR) | |
return | |
end | |
flag = flag or config.default_flag | |
local template_key = config.flags[flag] | |
if not template_key then | |
local available_flags = "" | |
for f, _ in pairs(config.flags) do | |
available_flags = available_flags .. " " .. f | |
end | |
vim.notify( | |
"Unknown flag '" .. flag .. "' for :" .. command .. ". Available flags:" .. available_flags, | |
vim.log.levels.ERROR | |
) | |
return | |
end | |
create_template(name, template_key, custom_args) | |
end, { nargs = "*" }) | |
end | |
pcall(vim.api.nvim_del_user_command, "Create") | |
vim.api.nvim_create_user_command("Create", function(opts) | |
local raw_args = opts.args or "" | |
local parts = vim.split(raw_args, " ", { trimempty = true }) | |
if #parts < 2 then | |
vim.notify("Usage: Create <template_type> <n> [args...]", vim.log.levels.ERROR) | |
return | |
end | |
local template_key = parts[1] | |
local name = parts[2] | |
local custom_args_str = nil | |
if #parts > 2 then | |
custom_args_str = table.concat(parts, " ", 3) | |
end | |
if not template_definitions[template_key] then | |
local available_templates = "" | |
for t, _ in pairs(template_definitions) do | |
available_templates = available_templates .. " " .. t | |
end | |
vim.notify( | |
"Unknown template type: " .. template_key .. ". Available templates:" .. available_templates, | |
vim.log.levels.ERROR | |
) | |
return | |
end | |
create_template(name, template_key, custom_args_str) | |
end, { | |
nargs = "*", | |
complete = function(_, line, _) | |
local words = vim.split(line, " ", { trimempty = true }) | |
if #words == 1 then | |
local templates = {} | |
for t, _ in pairs(template_definitions) do | |
table.insert(templates, t) | |
end | |
return templates | |
end | |
return {} | |
end, | |
}) | |
pcall(vim.api.nvim_del_user_command, "TemplateHelp") | |
vim.api.nvim_create_user_command("TemplateHelp", function(opts) | |
local template_key = opts.args | |
if not template_key or template_key == "" then | |
local help_text = "Available templates:\n" | |
for t, _ in pairs(template_definitions) do | |
help_text = help_text .. " " .. t .. "\n" | |
end | |
vim.notify(help_text, vim.log.levels.INFO) | |
return | |
end | |
local config = template_definitions[template_key] | |
if not config then | |
vim.notify("Unknown template type: " .. template_key, vim.log.levels.ERROR) | |
return | |
end | |
if #config.arguments == 0 then | |
vim.notify("Template '" .. template_key .. "' has no custom arguments.", vim.log.levels.INFO) | |
return | |
end | |
local help_text = "Custom arguments for '" .. template_key .. "':\n" | |
for _, arg in ipairs(config.arguments) do | |
help_text = help_text | |
.. " " | |
.. arg.name | |
.. ": " | |
.. arg.description | |
.. " (default: " | |
.. tostring(arg.default) | |
.. ")\n" | |
end | |
vim.notify(help_text, vim.log.levels.INFO) | |
end, { | |
nargs = "?", | |
complete = function(_, _, _) | |
local templates = {} | |
for t, _ in pairs(template_definitions) do | |
table.insert(templates, t) | |
end | |
return templates | |
end, | |
}) | |
end | |
return M |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment