Creating items from context-aware templates.
The idea came from the common feature of modern IDEs that, allowing to create a file based on template and context, things like Add new class in the menu while you right-click in the explorer. This plugin was designed to be a scaffold to write your own template with context-aware capabilities.
{
'sharpchen/new-item.nvim',
event = 'VeryLazy',
submodules = true,
config = function()
require('new-item').setup {
picker = {
name = 'snacks', -- or 'fzf-lua' or 'telescope'
preview = false,
}
}
vim.keymap.set('n', '<leader>ni', '<cmd>NewItem<CR>')
require('new-item.groups').my_group_name = {
cond = true,
items = {--[[ ... ]]}
}
-- IMPORTANT: load after all groups are defined
require('new-item').load_groups()
end
}An Item is a template knows how to create a thing, an ItemGroup is a dynamically conditioned container for items.
This plugin was written in a object-oriented style, each type of item was derived from new-item.Item, any kind of item has the following fields:
iname: identifier for the item.suffixandprefix: parts around the name of item.- for example, to create a typescript test file, the
suffixcan be.test.ts, and the final name would be<name>.test.ts.
- for example, to create a typescript test file, the
nameable: indicating whether the item can have a custom name.- for example, a
.gitignoreis always.gitignore, it should be notnameable. - if an item is not
nameable, it must have adefault_name.
- for example, a
default_name: a default value for the name, can evaluate dynamically.default_nameis the pre-filled input ofvim.ui.inputduring creation when the item isnameable.
cwd: the folder where the item would be created at, defaults to parent of current buffer.edit: presume the item created is a file, and open it after creation.extra_args; an array of argument names, each argument will have an input request during creation.- you can use these arguments in
before_creationto do transformation.
- you can use these arguments in
before_creation(item, ctx): to perform a transformation before actually creating the item- you may transform things like the content or path, even the item template itself(it's an copy of original one)
after_creation(item, ctx): to perform after creation
Item base definition
---@class new-item.Item
---@field label string Name displayed as entry in picker
---@field desc? string Description of the item
---@field invoke? fun(self: self) Activate the creation for this item
---@field cwd? fun(): string Returns which parent folder to create the file, default to parent of current buffer
---@field extra_args? string[] Extra argument names to be specified on creation
---@field before_creation? fun(self: new-item.AnyItem, ctx: new-item.ItemCreationContext)
---@field after_creation? fun(self: new-item.AnyItem, ctx: new-item.ItemCreationContext)
---@field nameable? boolean True if the file item should have a custom name on creation
---@field default_name? string | fun(): string Default name of the item to be created
---@field suffix? string Trailing content of the constructed item name. Can be file extension such as `.lua` or suffix like `.test.ts`
---@field prefix? string Leading content of the constructed item nameFileItem: creating item from string content.
FileItem definition
---@class (exact) new-item.FileItem : new-item.Item
---@field filetype? string
---@field content? string
---@field edit? boolean Use :edit to create a buffer with pre-fill content instead of direct creation
---@field link? string | fun(): string Use content from another existing file
---@overload fun(o: new-item.FileItem): new-item.FileItemlocal file = require('new-item.items').FileItem
require('new-item.groups').javascript:append { -- assuming javascript is a existing item group
file {
iname = 'javascript',
label = 'javascript file',
content = 'console.log("%s")', -- %s will be replaced by name input
filetype = 'javascript', -- for treesitter highlighting
suffix = '.js' -- extension of the file
}
file {
iname = 'prettierrc',
label = '.prettierrc',
edit = false, -- do not create the file directly but open a buffer with content
link = vim.fn.expand('~/.prettierrc'), -- use content of an existing file
nameable = false, -- .prettierrc is always .prettierrc
default_name = '.prettierrc',
filetype = 'json',
cwd = function() return vim.fn.getcwd() end, -- should always add to project root
},
}
require('new-item.groups').md:append {
-- use the file name as top level title
file {
iname = 'markdown',
label = 'Markdown file',
filetype = 'markdown',
suffix = '.md',
content = [[# %s]],
},
}CmdItem: creating item by executing a shell command, and we can presume the item created is a file.
CmdItem definition
---@class (exact) new-item.CmdItem : new-item.Item
---@field cmd string[]
---@field edit? boolean Whether to open the item after creation, default to true
---@field append_name? boolean whether to append ctx.name_input to item.cmd
---@overload fun(o: new-item.CmdItem): new-item.CmdItemThe following examples shows how it wrap dotnet new command as a template.
local cmd = require('new-item.items').CmdItem
require('new-item.groups').dotnet:append {
cmd {
iname = 'buildtargets',
label = 'Directory.Build.targets',
nameable = false,
default_name = 'Directory.Build.targets',
cmd = { 'dotnet', 'new', 'buildtargets' },
},
cmd {
iname = 'class',
label = 'class',
cmd = { 'dotnet', 'new', 'class', '-lang', 'C#', '--name' } -- argument of --name is not given
before_creation = function(item, ctx)
item.cmd = vim.list_extend(item.cmd, { ctx.name_input }) -- append name argument
end
},
-- or use append_name so you don't have to manually append it
cmd {
iname = 'slnx',
label = 'slnx',
cmd = { 'dotnet', 'new', 'sln', '--format', 'slnx', '--name' },
suffix = '.slnx',
append_name = true, -- implicitly append name_input to item.cmd
default_name = function() return vim.fs.basename(vim.fn.getcwd()) end, -- use root folder name as default
}
}You can transform item and ctx on before_creation to let it be a context-aware template.
A context contains temporary values generated during the creation, such as name_input, cwd etc.
ItemCreationContext definition
---@class new-item.ItemCreationContext
---@field name_input? string name specified from vim.ui.input
---@field args? table<string, string> args input from vim.ui.input
---@field path? string path of the item to be created
---@field cwd? string the folder where the item would be created atThe following example shows how to use a FileItem create a new C# class using its current folder structure as namespace.
local util = require('new-item.util')
require('new-item.groups').dotnet:append {
file {
label = 'class',
suffix = '.cs',
filetype = 'cs',
content = util.dedent([[
namespace <namespace>;
public class %s { }
]]),
before_creation = function(item, ctx)
local proj
vim.fs.root(ctx.cwd, function(name, path)
if name:match('%.%w+proj$') then proj = vim.fs.joinpath(path, name) end
end)
local root_ns, ns
vim.system({ 'dotnet', 'msbuild', proj, '-getProperty:RootNamespace' }, { text = true },
function(out)
if out.code == 0 then root_ns = vim.trim(out.stdout) end
end):wait()
local rel = vim.fs.relpath(vim.fs.dirname(proj), ctx.cwd)
if rel and rel ~= '.' then
ns = root_ns .. '.' .. rel:gsub('/', '.')
else
ns = root_ns
end
item.content = item.content:gsub('<namespace>', ns)
end,
},
}group.<iname>:override allows to modify the item specification with final and prev states.
final is the current state of the item(to be modified), prev is the original state of the item.
The following example is how you can append extra operation to before_creation phase of item buildprops, from dotnet group.
groups.dotnet.buildprops:override(function(final, prev)
final.before_creation = function(item, ctx)
-- additional operations...
prev.before_creation(item, ctx)
end
end)If loading a group involves asynchronous operation, you would need to bind a callback using ItemGroup.on_loaded to do the override.
groups.dotnet:on_loaded(function(self)
self.buildprops:override(function(final, prev)
final.before_creation = function(item, ctx)
-- additional operations...
prev.before_creation(item, ctx)
end
end)
end)Each item must be of certain group, each group has a cond field to be evaluated dynamically to indicate whether its contained items should present each time your invoke the picker.
cond(): boolean: indicating whether its items should present in picker.items: user-defined templates.builtin_items: pre-defined templates from the plugin or other sources.enable_builtin: whether to includebuiltin_itemsin picker.append(self, items): append extra templates toitemgroup.itemslist.
ItemGroup definition
---@class new-item.ItemGroup
---@field name? string
---@field cond? boolean | fun(): boolean
---@field items? new-item.AnyItem[]
---@field enable_builtin? boolean show builtin items
---@field private builtin_items? new-item.AnyItem[]
---@field append? fun(self, items: new-item.AnyItem[]) -- append user defined items
---@field get_items? fun(self): new-item.AnyItem[]For example, you may require javascript templates to present only when it found a package.json file on root.
require('new-item.groups').javascript = {
cond = function()
return vim.fs.root(vim.fn.expand('%:p:h'), 'package.json') ~= nil
end,
items = {--[[...]]}
}You can add any number of groups for your specific working environments.
ItemGroup was designed as a proxy table, so it has a dedicated method ItemGroup:override to alter its state.
That is, do not assign or alter any field to an ItemGroup with dot accessor, use override instead.
group:override {
cond = true,
enable_builtin = false
}- an item name was decided by either
ctx.name_inputordefault_name, depending on whether the template isnameable. - path of the item to be created was composed by the item name,
ctx.cwd,item.suffixanditem.prefix.- for
FileItem,%sinitem.contentwould be replaced byctx.name_input - for
CmdItem,ctx.name_inputwould be appended toitem.cmdwhenitem.append_nameistrue
- for
before_creationwas then triggered, might perform some transformation.item:invoke()was triggered to create the item.after_creationwas triggered to perform a post action.
gitignore: a.gitignorecollection from https://github.com/github/gitignore- this group is not presented in picker by default, because there's too many of them, use
:NewItem gitignoreto create one.
- this group is not presented in picker by default, because there's too many of them, use
gitattributes: a.gitattributescollection from https://github.com/gitattributes/gitattributes- this group is not presented in picker by default, because there's too many of them, use
:NewItem gitattributesto create one.
- this group is not presented in picker by default, because there's too many of them, use
dotnet: some wrappers fordotnet newtemplates.
