Skip to content

sharpchen/new-item.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

new-item.nvim

Creating items from context-aware templates.

new-item

Why this plugin

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.

Installation

{
  '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
}

Item & Item Group

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.
  • suffix and prefix: parts around the name of item.
    • for example, to create a typescript test file, the suffix can be .test.ts, and the final name would be <name>.test.ts.
  • nameable: indicating whether the item can have a custom name.
    • for example, a .gitignore is always .gitignore, it should be not nameable.
    • if an item is not nameable, it must have a default_name.
  • default_name: a default value for the name, can evaluate dynamically.
    • default_name is the pre-filled input of vim.ui.input during creation when the item is nameable.
  • 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_creation to do transformation.
  • 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 name

Writing items

  1. FileItem: 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.FileItem
local 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]],
  },
}
  1. 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.CmdItem

The 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
  }
}

Using transformation

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 at

The 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,
  },
}

Override Item

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)

Writing item group

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 include builtin_items in picker.
  • append(self, items): append extra templates to itemgroup.items list.
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.

Override ItemGroup

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
}

How does it work

  1. an item name was decided by either ctx.name_input or default_name, depending on whether the template is nameable.
  2. path of the item to be created was composed by the item name, ctx.cwd, item.suffix and item.prefix.
    • for FileItem, %s in item.content would be replaced by ctx.name_input
    • for CmdItem, ctx.name_input would be appended to item.cmd when item.append_name is true
  3. before_creation was then triggered, might perform some transformation.
  4. item:invoke() was triggered to create the item.
  5. after_creation was triggered to perform a post action.

Group Presets

  • gitignore: a .gitignore collection from https://github.com/github/gitignore
    • this group is not presented in picker by default, because there's too many of them, use :NewItem gitignore to create one.
  • gitattributes: a .gitattributes collection from https://github.com/gitattributes/gitattributes
    • this group is not presented in picker by default, because there's too many of them, use :NewItem gitattributes to create one.
  • dotnet: some wrappers for dotnet new templates.