10 KiB
Table Of Contents
Features
- Create annotations with one keybind, and jump your cursor using
Tab. - Defaults for multiple languages and annotation conventions
- Extremely customizable and extensible
- Written in lua
Requirements
- Install nvim-treesitter
Installation
Use your favorite package manager to install Neogen, e.g:
use {
"danymat/neogen",
config = function()
require('neogen').setup {
enabled = true
}
end,
requires = "nvim-treesitter/nvim-treesitter"
}
Usage
I exposed a function to generate the annotations.
require('neogen').generate()
You can bind it to your keybind of choice, like so:
local opts = { noremap = true, silent = true }
vim.api.nvim_set_keymap("n", "<Leader>nf", ":lua require('neogen').generate()<CR>", opts)
Calling the generate function without any parameters will try to generate annotations for the current function.
You can provide some options for the generate, like so:
require('neogen').generate({
type = "func" -- the annotation type to generate. Currently supported: func, class, type
})
For example, I can add an other keybind to generate class annotations:
local opts = { noremap = true, silent = true }
vim.api.nvim_set_keymap("n", "<Leader>nc", ":lua require('neogen').generate({ type = 'class' })<CR>", opts)
Cycle between annotations
I added support passing cursor positionings in templates.
That means you can now cycle your cursor between different parts of the annotation.
The default keybind is <C-e> in insert mode.
If you want to add <Tab> completion instead, be sure you don't have a completion plugin. If so, you have to configure them:
nvim-cmp
local cmp = require('cmp')
local neogen = require('neogen')
local t = function(str)
return vim.api.nvim_replace_termcodes(str, true, true, true)
end
local check_back_space = function()
local col = vim.fn.col '.' - 1
return col == 0 or vim.fn.getline('.'):sub(col, col):match '%s' ~= nil
end
cmp.setup {
...
-- You must set mapping if you want.
mapping = {
["<tab>"] = cmp.mapping(function(fallback)
if vim.fn.pumvisible() == 1 then
vim.fn.feedkeys(t("<C-n>"), "n")
elseif neogen.jumpable() then
vim.fn.feedkeys(t("<cmd>lua require('neogen').jump_next()<CR>"), "")
elseif check_back_space() then
vim.fn.feedkeys(t("<tab>"), "n")
else
fallback()
end
end, {
"i",
"s",
}),
},
...
}
Configuration
require('neogen').setup {
enabled = true, --if you want to disable Neogen
input_after_comment = true, -- (default: true) automatic jump (with insert mode) on inserted annotation
jump_map = "<C-e>" -- The keymap in order to jump in the annotation fields (in insert mode)
}
}
If you're not satisfied with the default configuration for a language, you can change the defaults like this:
require('neogen').setup {
enabled = true,
languages = {
lua = {
template = {
annotation_convention = "emmylua" -- for a full list of annotation_conventions, see supported-languages below,
... -- for more template configurations, see the language's configuration file in configurations/{lang}.lua
}
},
...
}
}
Supported Languages
There is a list of supported languages and fields, with their annotation style
| Language | Annotation conventions | Supported fields |
|---|---|---|
| lua | ||
Emmylua, Ldoc ("emmylua", "ldoc") |
@param, @varargs, @return, @class, @type |
|
| python | ||
Google docstrings ("google_docstrings") |
Args, Attributes, Returns |
|
Numpydoc ("numpydoc") |
Arguments, Attributes, Returns |
|
| javascript | ||
JSDoc ("jsdoc") |
@param, @returns, @class, @classdesc |
|
| c | ||
Doxygen ("doxygen") |
@param, @returns |
Adding Languages
Configuration file
The configuration file for a language is in lua/configurations/{lang}.lua.
Note: Be aware that Neogen uses Treesitter to operate. You can install TSPlayground to check the AST.
Below is a commented sample of the configuration file for lua.
-- Search for these nodes
parent = { "function", "local_function", "local_variable_declaration", "field", "variable_declaration" },
-- Traverse down these nodes and extract the information as necessary
data = {
-- If function or local_function is found as a parent
["function|local_function"] = {
-- Get second child from the parent node
["2"] = {
-- This second child has to be of type "parameters", otherwise does nothing
match = "parameters",
-- Extractor function that returns a set of TSname = values with values being of type string[]
extract = function(node)
local regular_params = neogen.utilities.extractors:extract_children_text("identifier")(node)
local varargs = neogen.utilities.extractors:extract_children_text("spread")(node)
return {
parameters = regular_params,
vararg = varargs,
}
end,
},
},
},
-- Custom lua locator that escapes from comments (More on locators below)
-- Passing nil will use the default locator
locator = require("neogen.locators.lua"),
-- Use default granulator and generator (More on them below)
granulator = nil,
generator = nil,
-- Template to use with the generator. (More on this below)
template = {
-- Which annotation convention to use
annotation_convention = "emmylua",
emmylua = {
{ nil, "- " },
{ "parameters", "- @param %s any" },
{ "vararg", "- @vararg any" },
{ "return_statement", "- @return any" }
}
},
The Neogen code is then divided in 3 major concepts:
Locators
A locator tries to find (from the cursor node) one of the nodes from parents field specified in configuration.
This is the signature of the function:
function(node_info, nodes_to_match)
return node
end
- With
node_infobeing a table with 2 fields:
{
root = root_node -- <TSnode>
current = current_node -- <TSnode>
}
nodes_to_matchis the field fromparentsin language configuration.
Default: The default locator (in lua/locators/default.lua) just go back to the parent node of the current one and sees if it's one of the requested parents.
Granulators
Now that a parent node is found (with locators) from the cursor location, it's time to use this node to find all requested fields.
The function signature is this:
function(parent_node, node_data)
return result
end
parent_nodebeing the node returned from the locatorresultis a table containing a set oftype = valueswith values from typestring[], and type being a TS node name.node_databeing the fielddatafrom configuration file. For example, if thedatafield is this one:
data = {
["function|local_function"] = {
-- get second child from the parent node
["2"] = {
-- it has to be of type "parameters"
match = "parameters",
extract = function(node)
local tree = {
{ retrieve = "all", node_type = "identifier", extract = true },
{ retrieve = "all", node_type = "spread", extract = true }
}
local nodes = neogen.utilities.nodes:matching_nodes_from(node, tree)
local res = neogen.utilities.extractors:extract_from_matched(nodes)
return {
parameters = res.identifier,
vararg = res.spread,
}
end,
},
},
}
Notes:
- If you create your own granulator, you can add any kind of parameters in the
datafield from configuration file as long as the function signature is the same provided. - Utilities are provided. You can check out their documentation in
lua/utilities/.
Generators
A generator takes in the results from the granulator and tries to generate the template according to the language's configuration.
This is the function signature for a generator:
function(parent, data, template)
return start_row, start_col, generated_template
end
parentis the parent node found with the locatordatais the result from the granulatortemplatebeing thetemplatefield from the language configuration file.start_rowis the row in which we will appendgenerated_templatestart_colis the col in which thegenerated_templatewill startgenerated_templateis the output we will append on the specified locations.
