Merge pull request #64 from kevinhwang91/refactor

BREAKING CHANGE (developers only)

- Deleted `jump_text` option, and made it default
- Deleted the ability to override generators and granulators
This commit is contained in:
Daniel Mathiot
2022-02-08 15:49:15 +01:00
committed by GitHub
27 changed files with 554 additions and 668 deletions

View File

@@ -112,23 +112,14 @@ Or, if you want to use a key that's already used for completion purposes, take a
local cmp = require('cmp') local cmp = require('cmp')
local neogen = require('neogen') 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 { cmp.setup {
... ...
-- You must set mapping if you want. -- You must set mapping if you want.
mapping = { mapping = {
["<tab>"] = cmp.mapping(function(fallback) ["<tab>"] = cmp.mapping(function(fallback)
if neogen.jumpable() then if require('neogen').jumpable() then
vim.fn.feedkeys(t("<cmd>lua require('neogen').jump_next()<CR>"), "") require('neogen').jump_next()
else else
fallback() fallback()
end end
@@ -137,8 +128,8 @@ cmp.setup {
"s", "s",
}), }),
["<S-tab>"] = cmp.mapping(function(fallback) ["<S-tab>"] = cmp.mapping(function(fallback)
if neogen.jumpable(-1) then if require('neogen').jumpable(true) then
vim.fn.feedkeys(t("<cmd>lua require('neogen').jump_prev()<CR>"), "") require('neogen').jump_prev()
else else
fallback() fallback()
end end

View File

@@ -95,11 +95,8 @@ Neogen provides those defaults, and you can change them to suit your needs
-- Go to annotation after insertion, and change to insert mode -- Go to annotation after insertion, and change to insert mode
input_after_comment = true, input_after_comment = true,
-- Symbol to find for jumping cursor in template
jump_text = "$1",
-- Configuration for default languages -- Configuration for default languages
languages = {}, languages = {}
} }
< <
# Notes~ # Notes~
@@ -112,9 +109,6 @@ Neogen provides those defaults, and you can change them to suit your needs
} }
< <
- `jump_text` is widely used and will certainly break most language templates.
I'm thinking of removing it from defaults so that it can't be modified
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
*neogen.generate()* *neogen.generate()*
`neogen.generate`({opts}) `neogen.generate`({opts})

23
lua/neogen/config.lua Normal file
View File

@@ -0,0 +1,23 @@
local config = {_data = {}}
config.get = function()
return config._data
end
config.setup = function(default, user)
local data = vim.tbl_deep_extend("keep", user or {}, default)
setmetatable(data.languages, {
__index = function(langs, ft)
local ok, ft_config = pcall(require, "neogen.configurations." .. ft)
if not ok then
ft_config = nil
end
rawset(langs, ft, ft_config)
return ft_config
end
})
config._data = data
return data
end
return config

View File

@@ -1,7 +1,7 @@
local extractors = require("neogen.utilities.extractors") local extractors = require("neogen.utilities.extractors")
local nodes_utils = require("neogen.utilities.nodes") local nodes_utils = require("neogen.utilities.nodes")
local default_locator = require("neogen.locators.default") local default_locator = require("neogen.locators.default")
local template = require("neogen.utilities.template") local template = require("neogen.template")
local i = require("neogen.types.template").item local i = require("neogen.types.template").item
local c_params = { local c_params = {
@@ -149,14 +149,14 @@ local c_config = {
return nil return nil
end end
if node_info.current == nil then if not node_info.current then
return result return result
end end
-- if the function happens to be a function template we want to place -- if the function happens to be a function template we want to place
-- the annotation before the template statement and extract the -- the annotation before the template statement and extract the
-- template parameters names as well -- template parameters names as well
if node_info.current:parent() == nil then if not node_info.current:parent() then
return result return result
end end
if node_info.current:parent():type() == "template_declaration" then if node_info.current:parent():type() == "template_declaration" then
@@ -165,10 +165,6 @@ local c_config = {
return result return result
end, end,
-- Use default granulator and generator
granulator = nil,
generator = nil,
template = template:add_default_annotation("doxygen"), template = template:add_default_annotation("doxygen"),
} }

View File

@@ -1,6 +1,6 @@
local extractors = require("neogen.utilities.extractors") local extractors = require("neogen.utilities.extractors")
local nodes_utils = require("neogen.utilities.nodes") local nodes_utils = require("neogen.utilities.nodes")
local template = require("neogen.utilities.template") local template = require("neogen.template")
local i = require("neogen.types.template").item local i = require("neogen.types.template").item
return { return {

View File

@@ -1,4 +1,4 @@
local template = require("neogen.utilities.template") local template = require("neogen.template")
return { return {
parent = { parent = {

View File

@@ -1,6 +1,6 @@
local extractors = require("neogen.utilities.extractors") local extractors = require("neogen.utilities.extractors")
local nodes_utils = require("neogen.utilities.nodes") local nodes_utils = require("neogen.utilities.nodes")
local template = require("neogen.utilities.template") local template = require("neogen.template")
local function_tree = { local function_tree = {
{ {

View File

@@ -1,7 +1,7 @@
local i = require("neogen.types.template").item local i = require("neogen.types.template").item
local extractors = require("neogen.utilities.extractors") local extractors = require("neogen.utilities.extractors")
local nodes_utils = require("neogen.utilities.nodes") local nodes_utils = require("neogen.utilities.nodes")
local template = require("neogen.utilities.template") local template = require("neogen.template")
local function_tree = { local function_tree = {
{ {

View File

@@ -1,7 +1,7 @@
local extractors = require("neogen.utilities.extractors") local extractors = require("neogen.utilities.extractors")
local i = require("neogen.types.template").item local i = require("neogen.types.template").item
local nodes_utils = require("neogen.utilities.nodes") local nodes_utils = require("neogen.utilities.nodes")
local template = require("neogen.utilities.template") local template = require("neogen.template")
local function_extractor = function(node, type) local function_extractor = function(node, type)
if not vim.tbl_contains({ "local", "function" }, type) then if not vim.tbl_contains({ "local", "function" }, type) then
@@ -145,9 +145,5 @@ return {
-- Custom lua locator that escapes from comments -- Custom lua locator that escapes from comments
locator = require("neogen.locators.lua"), locator = require("neogen.locators.lua"),
-- Use default granulator and generator
granulator = nil,
generator = nil,
template = template:config({ use_default_comment = true }):add_default_annotation("emmylua"):add_annotation("ldoc"), template = template:config({ use_default_comment = true }):add_default_annotation("emmylua"):add_annotation("ldoc"),
} }

View File

@@ -1,6 +1,6 @@
local extractors = require("neogen.utilities.extractors") local extractors = require("neogen.utilities.extractors")
local nodes_utils = require("neogen.utilities.nodes") local nodes_utils = require("neogen.utilities.nodes")
local template = require("neogen.utilities.template") local template = require("neogen.template")
local i = require("neogen.types.template").item local i = require("neogen.types.template").item
return { return {

View File

@@ -2,7 +2,7 @@ local ts_utils = require("nvim-treesitter.ts_utils")
local nodes_utils = require("neogen.utilities.nodes") local nodes_utils = require("neogen.utilities.nodes")
local extractors = require("neogen.utilities.extractors") local extractors = require("neogen.utilities.extractors")
local locator = require("neogen.locators.default") local locator = require("neogen.locators.default")
local template = require("neogen.utilities.template") local template = require("neogen.template")
local i = require("neogen.types.template").item local i = require("neogen.types.template").item
local parent = { local parent = {

View File

@@ -1,7 +1,7 @@
local extractors = require("neogen.utilities.extractors") local extractors = require("neogen.utilities.extractors")
local i = require("neogen.types.template").item local i = require("neogen.types.template").item
local nodes_utils = require("neogen.utilities.nodes") local nodes_utils = require("neogen.utilities.nodes")
local template = require("neogen.utilities.template") local template = require("neogen.template")
return { return {
parent = { parent = {

View File

@@ -1,7 +1,7 @@
local extractors = require("neogen.utilities.extractors") local extractors = require("neogen.utilities.extractors")
local nodes_utils = require("neogen.utilities.nodes") local nodes_utils = require("neogen.utilities.nodes")
local i = require("neogen.types.template").item local i = require("neogen.types.template").item
local template = require("neogen.utilities.template") local template = require("neogen.template")
local construct_type_annotation = function(parameters) local construct_type_annotation = function(parameters)
local results = parameters and {} or nil local results = parameters and {} or nil

227
lua/neogen/generator.lua Normal file
View File

@@ -0,0 +1,227 @@
local helpers = require("neogen.utilities.helpers")
local notify = helpers.notify
local ok, ts_utils = pcall(require, "nvim-treesitter.ts_utils")
if not ok then
notify("neogen requires nvim-treesitter to operate :(", vim.log.levels.ERROR)
return function(_, _) end
end
local conf = require("neogen.config").get()
local granulator = require("neogen.granulator")
local mark = require("neogen.mark")
local nodes = require("neogen.utilities.nodes")
local default_locator = require("neogen.locators.default")
local JUMP_TEXT = "$1"
local function get_parent_node(filetype, typ, language)
local parser = vim.treesitter.get_parser(0, filetype)
local tstree = parser:parse()[1]
local tree = tstree:root()
language.locator = language.locator or default_locator
-- Use the language locator to locate one of the required parent nodes above the cursor
return language.locator({root = tree, current = ts_utils.get_node_at_cursor(0)}, language.parent[typ])
end
--- Generates the prefix according to `template` options.
--- Prefix is generated with an offset (currently spaces) repetition of `n` times.
--- If `template.use_default_comment` is not set to false, the `commentstring` is added
---@param template table
---@param commentstring string
---@param n integer
---@return string
local function prefix_generator(template, commentstring, n)
local prefix = (" "):rep(n)
-- Do not append the comment string if not wanted
if template.use_default_comment ~= false then
prefix = prefix .. commentstring
end
return prefix
end
local function get_place_pos(parent, position, append, typ)
local row, col
-- You can use a custom position placement
if position and type(position) == "function" then
row, col = position(parent, typ)
end
-- If the custom placement does not return the correct row and cols, default to the node range
-- Same if there is no custom placement
if not row and not col then
-- Because the file type is always at top
if typ == "file" then
row, col = 0, 0
else
row, col = ts_utils.get_node_range(parent)
end
end
append = append or {}
if append.position == "after" and not vim.tbl_contains(append.disabled or {}, typ) then
local child_node = nodes:matching_child_nodes(parent, append.child_name)[1]
if not child_node and append.fallback then
local fallback = nodes:matching_child_nodes(parent, append.fallback)[1]
if fallback then
row, col = fallback:range()
end
else
row, col = child_node:range()
end
end
return row, col
end
--- Uses the provided template to format the annotations with data found by the granulator
---@param parent userdata the node used to generate the annotations
---@param data table the data from the granulator, which is a set of [type] = results
---@param template table a template from the configuration
---@param required_type string
---@return table { line, content }, with line being the line to append the content
local function generate_content(parent, data, template, required_type)
local row, col = get_place_pos(parent, template.position, template.append, required_type)
local commentstring = vim.trim(vim.bo.commentstring:format(""))
local generated_template = template[template.annotation_convention]
local result = {}
local prefix = prefix_generator(template, commentstring, col)
local function append_str(str)
table.insert(result, str == "" and str or prefix .. str)
end
for _, values in ipairs(generated_template) do
local inserted_type, formatted_str, opts = unpack(values)
opts = opts or {}
if type(opts.type) ~= "table" or vim.tbl_contains(opts.type, required_type) then
-- Will append the item before all their nodes
if type(opts.before_first_item) == "table" and data[inserted_type] then
for _, s in ipairs(opts.before_first_item) do
append_str(s)
end
end
local ins_type = type(inserted_type)
if ins_type == "nil" then
local no_data = vim.tbl_isempty(data)
if opts.no_results then
if no_data then
append_str(formatted_str)
end
elseif not no_data then
append_str(formatted_str:format(""))
end
elseif ins_type == "string" and type(data[inserted_type]) == "table" then
-- Format the output with the corresponding data
for _, s in ipairs(data[inserted_type]) do
append_str(formatted_str:format(s))
if opts.after_each then
append_str(opts.after_each:format(s))
end
end
elseif ins_type == "table" and #inserted_type > 0 and type(data[opts.required]) == "table" then
-- First item in the template item can be a table.
-- in this case, the template will use provided types to generate the line.
-- e.g {{ "type", "parameter"}, "* @type {%s} %s"}
-- will replace every %s with the provided data from those types
-- If one item is missing, it'll use the required option to iterate
-- and will replace the missing item with JUMP_TEXT
for _, extracted in pairs(data[opts.required]) do
local fmt_args = {}
for _, typ in ipairs(inserted_type) do
if extracted[typ] then
table.insert(fmt_args, extracted[typ][1])
else
table.insert(fmt_args, JUMP_TEXT)
end
end
append_str(formatted_str:format(unpack(fmt_args)))
if opts.after_each then
append_str(opts.after_each:format(unpack(fmt_args)))
end
end
end
end
end
return row, result
end
return setmetatable({}, {
__call = function(_, filetype, typ)
if filetype == "" then
notify("No filetype detected", vim.log.levels.WARN)
return
end
local language = conf.languages[filetype]
if not language then
notify("Language " .. filetype .. " not supported.", vim.log.levels.WARN)
return
end
typ = (type(typ) ~= "string" or typ == "") and "func" or typ -- Default type
local template = language.template
if not language.parent[typ] or not language.data[typ] or not template or not template.annotation_convention then
notify("Type `" .. typ .. "` not supported", vim.log.levels.WARN)
return
end
local parent_node = get_parent_node(filetype, typ, language)
if not parent_node then
return
end
local data = granulator(parent_node, language.data[typ])
-- Will try to generate the documentation from a template and the data found from the granulator
local row, template_content = generate_content(parent_node, data, template, typ)
local content = {}
local marks_pos = {}
local input_after_comment = conf.input_after_comment
local pattern = JUMP_TEXT .. (input_after_comment and "[|%w]*" or "|?")
for r, line in ipairs(template_content) do
local last_col = 0
local len = 1
local sects = {}
while true do
local s, e = line:find(pattern, last_col + 1)
if not s then
table.insert(sects, line:sub(last_col + 1, -1))
break
end
table.insert(sects, line:sub(last_col + 1, s - 1))
if input_after_comment then
len = len + s - last_col - 1
table.insert(marks_pos, {row + r - 1, len - 1})
end
last_col = e
end
table.insert(content, table.concat(sects, ""))
end
-- Append content to row
vim.api.nvim_buf_set_lines(0, row, row, true, content)
if #marks_pos > 0 then
-- Start session of marks
mark:start()
for _, pos in ipairs(marks_pos) do
mark:add_mark(pos)
end
vim.cmd("startinsert")
mark:jump()
-- Add range mark after first jump
mark:add_range_mark({row, 0, row + #template_content, 1})
end
end
})

View File

@@ -1,203 +0,0 @@
local ts_utils = require("nvim-treesitter.ts_utils")
local nodes = require("neogen.utilities.nodes")
--- Generates the prefix according to `template` options.
--- Prefix is generated with an offset (currently spaces) repetition of `n` times.
--- If `template.use_default_comment` is not set to false, the `commentstring` is added
--- @param template table
--- @param commentstring string
--- @param n integer
--- @return string
local function prefix_generator(template, commentstring, n)
local prefix = (" "):rep(n)
-- Do not append the comment string if not wanted
if template.use_default_comment ~= false then
prefix = prefix .. commentstring
end
return prefix
end
--- Does some checks within the `value` and adds the `prefix` before it if required
--- @param prefix string
--- @param value string
--- @return string
local function conditional_prefix_inserter(prefix, value)
if value == "" then
return value
else
return prefix .. value
end
end
--- Insert values from `items` in `result` and returns it
--- @param result table
--- @param items table
--- @param prefix string
--- @return table result
local function add_values_to_result(result, items, prefix)
for _, value in ipairs(items) do
local inserted = conditional_prefix_inserter(prefix, value)
table.insert(result, inserted)
end
return result
end
---Default Generator:
---Uses the provided template to format the annotations with data found by the granulator
--- @param parent userdata the node used to generate the annotations
--- @param data table the data from the granulator, which is a set of [type] = results
--- @param template table a template from the configuration
--- @param required_type string
--- @return table { line, content, opts }, with line being the line to append the content
return function(parent, data, template, required_type)
local row_to_place, col_to_place
-- You can use a custom position placement
if template.position and type(template.position) == "function" then
row_to_place, col_to_place = template.position(parent, required_type)
end
-- If the custom placement does not return the correct row and cols, default to the node range
-- Same if there is no custom placement
if not row_to_place and not col_to_place then
-- Because the file type is always at top
if required_type == "file" then
row_to_place = 0
col_to_place = 0
else
row_to_place, col_to_place, _, _ = ts_utils.get_node_range(parent)
end
end
local commentstring, generated_template = vim.trim(vim.api.nvim_buf_get_option(0, "commentstring"):format(""))
local append = template.append or {}
if append.position == "after" and not vim.tbl_contains(append.disabled or {}, required_type) then
local child_node = nodes:matching_child_nodes(parent, append.child_name)[1]
if not child_node and append.fallback then
local fallback = nodes:matching_child_nodes(parent, append.fallback)[1]
if fallback then
row_to_place, col_to_place, _, _ = fallback:range()
end
else
row_to_place, col_to_place, _, _ = child_node:range()
end
end
if not template or not template.annotation_convention then
-- Default template
generated_template = {
{ nil, "" },
{ "name", " @Summary " },
{ "parameters", " @Param " },
{ "return", " @Return " },
}
elseif type(template) == "function" then
-- You can also pass a function as a template
generated_template = template(parent, commentstring, data)
else
generated_template = template[template.annotation_convention]
end
local function parse_generated_template()
local result = {}
local prefix = prefix_generator(template, commentstring, col_to_place)
for _, values in ipairs(generated_template) do
local inserted_type = values[1]
local formatted_string = values[2]
local opts = vim.deepcopy(values[3]) or {}
opts.type = opts.type or { required_type }
if opts.type and vim.tbl_contains(opts.type, required_type) then
-- Will append the item before all their nodes
if opts.before_first_item and data[inserted_type] then
result = add_values_to_result(result, opts.before_first_item, prefix)
end
-- If there is no data returned, will append the string with opts.no_results
if opts.no_results == true and vim.tbl_isempty(data) then
local inserted = conditional_prefix_inserter(prefix, formatted_string)
table.insert(result, inserted)
else
-- append the output as is
if inserted_type == nil and opts.no_results ~= true and not vim.tbl_isempty(data) then
local inserted = conditional_prefix_inserter(prefix, formatted_string:format(""))
table.insert(result, inserted)
elseif inserted_type then
-- Format the output with the corresponding data
if type(inserted_type) == "string" and data[inserted_type] then
for _, value in ipairs(data[inserted_type]) do
local inserted = conditional_prefix_inserter(prefix, formatted_string:format(value))
table.insert(result, inserted)
if opts.after_each then
table.insert(
result,
conditional_prefix_inserter(prefix, opts.after_each):format(value)
)
end
end
elseif type(inserted_type) == "table" and data[opts.required] then
-- First item in the template item can be a table.
-- in this case, the template will use provided types to generate the line.
-- e.g {{ "type", "parameter"}, "* @type {%s} %s"}
-- will replace every %s with the provided data from those types
-- If one item is missing, it'll use the required option to iterate
-- and will replace the missing item with default jump_text
for _, tbl in pairs(data[opts.required]) do
local _values = {}
for _, v in ipairs(inserted_type) do
if tbl[v] then
table.insert(_values, tbl[v][1])
else
local jump_text = neogen.configuration.jump_text
table.insert(_values, jump_text)
end
end
if not vim.tbl_isempty(_values) then
local inserted = conditional_prefix_inserter(
prefix,
formatted_string:format(unpack(_values))
)
table.insert(result, inserted)
if opts.after_each then
if type(opts.after_each) == "table" then
local _v = {}
local index_types = opts.after_each["index_types"]
for _, i in ipairs(index_types) do
table.insert(_v, _values[i])
end
_values = _v
table.insert(
result,
conditional_prefix_inserter(
prefix,
opts.after_each[1]:format(unpack(_values))
)
)
else
table.insert(
result,
conditional_prefix_inserter(
prefix,
opts.after_each:format(unpack(_values))
)
)
end
end
end
end
end
end
end
end
end
return result
end
return row_to_place, col_to_place, parse_generated_template()
end

45
lua/neogen/granulator.lua Normal file
View File

@@ -0,0 +1,45 @@
local ts_utils = require("nvim-treesitter.ts_utils")
local helpers = require("neogen.utilities.helpers")
--- Tries to use the configuration to find all required content nodes from the parent node
---@param parent_node userdata the node found by the locator
---@param node_data table the data from configurations[lang].data
return function(parent_node, node_data)
local result = {}
if not parent_node then
return result
end
for parent_type, child_data in pairs(node_data) do
local matches = helpers.split(parent_type, "|", true)
-- Look if the parent node is one of the matches
if vim.tbl_contains(matches, parent_node:type()) then
-- For each child_data in the matched parent node
for i, data in pairs(child_data) do
local index = tonumber(i)
assert(index, "Need a valid index")
local child_node = index == 0 and parent_node or parent_node:named_child(index - 1)
if not child_node then
return
end
if not data.match or child_node:type() == data.match then
if type(data.extract) == "function" then
-- Extract content from it { [type] = { data } }
for type, extracted_data in pairs(data.extract(child_node)) do
result[type] = extracted_data
end
else
-- if not extract function, get the text from the node (required: data.type)
result[data.type] = ts_utils.get_node_text(child_node)
end
end
end
end
end
return result
end

View File

@@ -1,61 +0,0 @@
local ts_utils = require("nvim-treesitter.ts_utils")
--- Tries to use the configuration to find all required content nodes from the parent node
--- @param parent_node userdata the node found by the locator
--- @param node_data table the data from configurations[lang].data
return function(parent_node, node_data)
local result = {}
for parent_type, child_data in pairs(node_data) do
local matches = vim.split(parent_type, "|", true)
-- Look if the parent node is one of the matches
if vim.tbl_contains(matches, parent_node:type()) then
-- For each child_data in the matched parent node
for i, data in pairs(child_data) do
local child_node
if tonumber(i) == 0 then
child_node = parent_node
else
child_node = parent_node:named_child(tonumber(i) - 1)
end
if not child_node then
return
end
if child_node:type() == data.match or not data.match then
local extract = {}
if data.extract then
-- Extract content from it { [type] = { data } }
extract = data.extract(child_node)
-- All extracted values are created added in result, like so: [data.type] = { extract }
if data.type then
-- Extract information into a one-dimensional array
local one_dimensional_arr = {}
for _, values in pairs(extract) do
table.insert(one_dimensional_arr, values)
end
result[data.type] = one_dimensional_arr
else
for type, extracted_data in pairs(extract) do
result[type] = extracted_data
end
end
else
-- if not extract function, get the text from the node (required: data.type)
extract = ts_utils.get_node_text(child_node)
result[data.type] = extract
end
end
end
end
end
return result
end

View File

@@ -1,6 +1,3 @@
local ok, ts_utils = pcall(require, "nvim-treesitter.ts_utils")
assert(ok, "neogen requires nvim-treesitter to operate :(")
--- Table of contents: --- Table of contents:
---@toc ---@toc
---@text ---@text
@@ -22,22 +19,15 @@ assert(ok, "neogen requires nvim-treesitter to operate :(")
--- - Written in lua (and uses Tree-sitter) --- - Written in lua (and uses Tree-sitter)
---@tag neogen ---@tag neogen
---@toc_entry Neogen's purpose ---@toc_entry Neogen's purpose
-- Requires =================================================================== -- Requires ===================================================================
local neogen = {} local neogen = {}
local conf
local config = require("neogen.config")
local helpers = require("neogen.utilities.helpers") local helpers = require("neogen.utilities.helpers")
local mark = require("neogen.mark")
local notify = helpers.notify local notify = helpers.notify
local cursor = require("neogen.utilities.cursor")
local default_locator = require("neogen.locators.default")
local default_granulator = require("neogen.granulators.default")
local default_generator = require("neogen.generators.default")
local autocmd = require("neogen.utilities.autocmd")
-- Module definition ========================================================== -- Module definition ==========================================================
--- Module setup --- Module setup
@@ -47,20 +37,10 @@ local autocmd = require("neogen.utilities.autocmd")
---@usage `require('neogen').setup({})` (replace `{}` with your `config` table) ---@usage `require('neogen').setup({})` (replace `{}` with your `config` table)
---@toc_entry The setup function ---@toc_entry The setup function
neogen.setup = function(opts) neogen.setup = function(opts)
-- Stores the user configuration globally so that we keep his configs when switching languages conf = config.setup(neogen.configuration, opts)
neogen.user_configuration = opts or {} if conf.enabled then
neogen.configuration = vim.tbl_deep_extend("keep", neogen.user_configuration, neogen.configuration)
if neogen.configuration.enabled == true then
neogen.generate_command() neogen.generate_command()
end end
-- Export module
_G.neogen = neogen
-- Force configuring current language again when doing `setup` call.
helpers.switch_language()
end end
--- Neogen Usage --- Neogen Usage
@@ -117,8 +97,6 @@ end
--- } --- }
--- < --- <
--- ---
--- - `jump_text` is widely used and will certainly break most language templates.
--- I'm thinking of removing it from defaults so that it can't be modified
---@toc_entry Configure the setup ---@toc_entry Configure the setup
---@tag neogen-configuration ---@tag neogen-configuration
neogen.configuration = { neogen.configuration = {
@@ -128,9 +106,6 @@ neogen.configuration = {
-- Go to annotation after insertion, and change to insert mode -- Go to annotation after insertion, and change to insert mode
input_after_comment = true, input_after_comment = true,
-- Symbol to find for jumping cursor in template
jump_text = "$1",
-- Configuration for default languages -- Configuration for default languages
languages = {}, languages = {},
} }
@@ -149,128 +124,12 @@ neogen.configuration = {
--- Currently supported: `func`, `class`, `type`, `file` --- Currently supported: `func`, `class`, `type`, `file`
---@toc_entry Generate annotations ---@toc_entry Generate annotations
neogen.generate = function(opts) neogen.generate = function(opts)
opts = opts or {} if not conf.enabled then
opts.type = (opts.type == nil or opts.type == "") and "func" or opts.type -- Default type
if not neogen.configuration.enabled then
notify("Neogen not enabled. Please enable it.", vim.log.levels.WARN) notify("Neogen not enabled. Please enable it.", vim.log.levels.WARN)
return return
end end
if vim.bo.filetype == "" then require("neogen.generator")(vim.bo.filetype, opts and opts.type)
notify("No filetype detected", vim.log.levels.WARN)
return
end
local parser = vim.treesitter.get_parser(0, vim.bo.filetype)
local tstree = parser:parse()[1]
local tree = tstree:root()
local language = neogen.configuration.languages[vim.bo.filetype]
if not language then
notify("Language " .. vim.bo.filetype .. " not supported.", vim.log.levels.WARN)
return
end
language.locator = language.locator or default_locator
language.granulator = language.granulator or default_granulator
language.generator = language.generator or default_generator
if not language.parent[opts.type] or not language.data[opts.type] then
notify("Type `" .. opts.type .. "` not supported", vim.log.levels.WARN)
return
end
-- Use the language locator to locate one of the required parent nodes above the cursor
local located_parent_node = language.locator({
root = tree,
current = ts_utils.get_node_at_cursor(0),
}, language.parent[opts.type])
if not located_parent_node then
return
end
-- Use the language granulator to get the required content inside the node found with the locator
local data = language.granulator(located_parent_node, language.data[opts.type])
if data then
-- Will try to generate the documentation from a template and the data found from the granulator
local to_place, start_column, content = language.generator(
located_parent_node,
data,
language.template,
opts.type
)
if #content ~= 0 then
cursor.del_extmarks() -- Delete previous extmarks before setting any new ones
local jump_text = language.jump_text or neogen.configuration.jump_text
--- Removes jump_text marks and keep the second part of jump_text|other_text if there is one (which is other_text)
local delete_marks = function(v)
local pattern = jump_text .. "[|%w]+"
local matched = string.match(v, pattern)
if matched then
local split = vim.split(matched, "|", true)
if #split == 2 and neogen.configuration.input_after_comment == false then
return string.gsub(v, jump_text .. "|", "")
end
else
return string.gsub(v, jump_text, "")
end
return string.gsub(v, pattern, "")
end
local content_with_marks = vim.deepcopy(content)
-- delete all jump_text marks
content = vim.tbl_map(delete_marks, content)
-- Append the annotation in required place
vim.fn.append(to_place, content)
-- Place cursor after annotations and start editing
-- First and last extmarks are needed to know the range of inserted content
if neogen.configuration.input_after_comment == true then
-- Creates extmark for the beggining of the content
cursor.create(to_place + 1, start_column)
-- Creates extmarks for the content
for i, value in pairs(content_with_marks) do
local start = 0
local count = 0
while true do
start = string.find(value, jump_text, start + 1)
if not start then
break
end
cursor.create(to_place + i, start - count * #jump_text)
count = count + 1
end
end
-- Create extmark to jump back to current location
local pos = vim.api.nvim_win_get_cursor(0)
local col = pos[2] + 2
-- If the line we are in is empty, it will throw an error out of bounds.
if vim.api.nvim_get_current_line() == "" then
col = pos[2] + 1
end
cursor.create(pos[1], col)
-- Creates extmark for the end of the content
cursor.create(to_place + #content + 1, 0)
cursor.jump({ first_time = true })
end
end
end
end end
-- Expose more API ============================================================ -- Expose more API ============================================================
@@ -279,18 +138,15 @@ end
neogen.match_commands = helpers.match_commands neogen.match_commands = helpers.match_commands
--- Get a template for a particular filetype --- Get a template for a particular filetype
---@param filetype string ---@param filetype? string
---@return neogen.TemplateConfig|nil ---@return neogen.TemplateConfig|nil
neogen.get_template = function(filetype) neogen.get_template = function(filetype)
if not neogen.configuration.languages[filetype] then local template
return local ft_conf = filetype and conf.languages[filetype] or conf.languages[vim.bo.filetype]
if ft_conf and ft_conf.template then
template = ft_conf.template
end end
return template
if not neogen.configuration.languages[filetype].template then
return
end
return neogen.configuration.languages[filetype].template
end end
-- Required for use with completion engine ===================================== -- Required for use with completion engine =====================================
@@ -298,24 +154,20 @@ end
--- Jumps to the next cursor template position --- Jumps to the next cursor template position
---@private ---@private
function neogen.jump_next() function neogen.jump_next()
if neogen.jumpable() then mark:jump()
cursor.jump()
end
end end
--- Jumps to the next cursor template position --- Jumps to the next cursor template position
---@private ---@private
function neogen.jump_prev() function neogen.jump_prev()
if cursor.jumpable(-1) then mark:jump(true)
cursor.jump_prev()
end
end end
--- Checks if the cursor can jump backwards or forwards --- Checks if the cursor can jump backwards or forwards
--- @param reverse number? if `-1`, will try to see if can be jumped backwards --- @param reverse number? if `-1` or true, will try to see if can be jumped backwards
---@private ---@private
function neogen.jumpable(reverse) function neogen.jumpable(reverse)
return cursor.jumpable(reverse) return mark:jumpable(reverse == -1 or reverse == true)
end end
-- Command and autocommands ==================================================== -- Command and autocommands ====================================================
@@ -323,22 +175,11 @@ end
--- Generates the `:Neogen` command, which calls `neogen.generate()` --- Generates the `:Neogen` command, which calls `neogen.generate()`
---@private ---@private
function neogen.generate_command() function neogen.generate_command()
vim.api.nvim_command( vim.cmd([[
'command! -nargs=? -complete=customlist,v:lua.neogen.match_commands -range -bar Neogen lua require("neogen").generate({ type = <q-args>})' command! -nargs=? -complete=customlist,v:lua.require'neogen'.match_commands -range -bar Neogen lua require("neogen").generate({ type = <q-args>})
) ]])
end end
autocmd.subscribe("BufEnter", function()
helpers.switch_language()
end)
vim.cmd([[
augroup ___neogen___
autocmd!
autocmd BufEnter * lua require'neogen.utilities.autocmd'.emit('BufEnter')
augroup END
]])
--- Contribute to Neogen --- Contribute to Neogen
--- ---
--- * Want to add a new language? --- * Want to add a new language?

View File

@@ -1,12 +1,12 @@
--- @class Neogen.node_info ---@class Neogen.node_info
--- @field current userdata the current node from cursor ---@field current userdata the current node from cursor
--- @field root? userdata the root node ---@field root? userdata the root node
--- The default locator tries to find one of the nodes to match in the current node --- The default locator tries to find one of the nodes to match in the current node
--- If it does not find one, will fetch the parents until he finds one --- If it does not find one, will fetch the parents until he finds one
--- @param node_info Neogen.node_info a node informations ---@param node_info Neogen.node_info a node informations
--- @param nodes_to_match table a list of parent nodes to match ---@param nodes_to_match table a list of parent nodes to match
--- @return userdata node one of the nodes to match directly above the given node ---@return userdata node one of the nodes to match directly above the given node
return function(node_info, nodes_to_match) return function(node_info, nodes_to_match)
if not node_info.current then if not node_info.current then
if vim.tbl_contains(nodes_to_match, node_info.root:type()) then if vim.tbl_contains(nodes_to_match, node_info.root:type()) then

162
lua/neogen/mark.lua Normal file
View File

@@ -0,0 +1,162 @@
---@class Mark
---@field started boolean
---@field bufnr number
---@field winid number
---@field index number
---@field last_cursor number
---@field ids number[]
---@field range_id number
local mark = {}
local api = vim.api
local ns = api.nvim_create_namespace("neogen")
local function compare_pos(p1, p2)
return p1[1] == p2[1] and p1[2] - p2[2] or p1[1] - p2[1]
end
--- Start marks creation and get useful informations
---@private
mark.start = function(self)
if self.started then
mark:stop()
end
self.bufnr = api.nvim_get_current_buf()
self.winid = api.nvim_get_current_win()
self.index = 0
local pos = api.nvim_win_get_cursor(self.winid)
local row, col = unpack(pos)
self.last_cursor_id = api.nvim_buf_set_extmark(self.bufnr, ns, row - 1, col, {})
self.ids = {}
self.range_id = nil
self.started = true
end
mark.valid = function(self)
return self.bufnr == api.nvim_get_current_buf() and self.winid == api.nvim_get_current_win()
end
--- Get a mark specified with i index
---@param i number
---@private
mark.get_mark = function(self, i)
local id = self.ids[i]
return api.nvim_buf_get_extmark_by_id(self.bufnr, ns, id, {})
end
--- Add a mark with position
---@param pos table Position as line, col
---@return number the id of the inserted mark
---@private
mark.add_mark = function(self, pos)
local line, col = unpack(pos)
local id = api.nvim_buf_set_extmark(self.bufnr, ns, line, col, {})
table.insert(mark.ids, id)
return id
end
--- Get how many marks are created
---@return number
---@private
mark.mark_len = function(self)
return #self.ids
end
mark.get_range_mark = function(self)
local d = api.nvim_buf_get_extmark_by_id(self.bufnr, ns, self.range_id, {details = true})
local row, col, end_row, end_col = d[1], d[2], d[3].end_row, d[3].end_col
return row, col, end_row, end_col
end
mark.add_range_mark = function(self, range)
local row, col, end_row, end_col = unpack(range)
self.range_id = api.nvim_buf_set_extmark(self.bufnr, ns, row, col,
{end_row = end_row, end_col = end_col})
end
mark.cursor_in_range = function(self, validated)
local ret = validated or self:valid()
if ret and self.range_id then
local pos = api.nvim_win_get_cursor(self.winid)
pos[1] = pos[1] - 1
local row, col, end_row, end_col = self:get_range_mark()
ret = compare_pos({row, col}, pos) <= 0 and compare_pos({end_row, end_col}, pos) >= 0
end
return ret
end
--- Verify if the marks can be jumpable
---@param reverse boolean
---@return boolean
---@private
mark.jumpable = function(self, reverse)
if not self.started then
return false
end
local validated = self:valid()
if not validated then
self:stop()
return validated
end
local ret
if reverse then
ret = self.index > 0
else
ret = #self.ids >= self.index
end
if ret then
ret = self:cursor_in_range(true)
end
if not ret then
self:jump_last_cursor(true)
self:stop()
end
return ret
end
--- Jump to next/previous mark if possible
---@param reverse boolean
---@private
mark.jump = function(self, reverse)
if self.started then
self.index = reverse and self.index - 1 or self.index + 1
end
if mark:jumpable(reverse) then
local line, row = unpack(self:get_mark(self.index))
api.nvim_win_set_cursor(self.winid, {line + 1, row})
end
end
mark.jump_last_cursor = function(self, validated)
if self:cursor_in_range(validated) then
local winid = self.winid
local pos = api.nvim_buf_get_extmark_by_id(self.bufnr, ns, self.last_cursor_id, {})
local line, col = unpack(pos)
api.nvim_win_set_cursor(winid, {line + 1, col})
end
end
--- Clear marks and stop jumping ability
---@private
mark.stop = function(self)
local bufnr = self.bufnr
if bufnr and bufnr > 0 and api.nvim_buf_is_valid(bufnr) then
for _, id in ipairs(self.ids) do
api.nvim_buf_del_extmark(bufnr, ns, id)
end
api.nvim_buf_del_extmark(bufnr, ns, self.last_cursor_id)
if self.range_id then
api.nvim_buf_del_extmark(bufnr, ns, self.range_id)
end
end
self.bufnr = nil
self.winid = nil
self.index = nil
self.last_cursor_id = nil
self.started = false
self.ids = {}
self.range_id = nil
end
return mark

View File

@@ -23,7 +23,7 @@ local autocmd = {}
autocmd.events = {} autocmd.events = {}
---Subscribe autocmd --- Subscribe autocmd
---@param event string ---@param event string
---@param callback function ---@param callback function
---@return function ---@return function
@@ -40,7 +40,7 @@ autocmd.subscribe = function(event, callback)
end end
end end
---Emit autocmd --- Emit autocmd
---@param event string ---@param event string
autocmd.emit = function(event) autocmd.emit = function(event)
autocmd.events[event] = autocmd.events[event] or {} autocmd.events[event] = autocmd.events[event] or {}

View File

@@ -1,110 +0,0 @@
cursor = {}
local neogen_ns = vim.api.nvim_create_namespace("neogen")
local neogen_virt_text_ns = vim.api.nvim_create_namespace("neogen_virt_text")
local current_position = 1
--- Wrapper around set_extmark with 1-based numbering for `line` and `col`, and returns the id of the created extmark
--- @param line number
--- @param col number
--- @return number
cursor.create = function(line, col)
current_position = 1
local new_col = col == 0 and 0 or col - 1
return vim.api.nvim_buf_set_extmark(0, neogen_ns, line - 1, new_col, {})
end
--- Find next created extmark and goes to it.
--- It removes the extmark afterwards.
--- First jumpable extmark is the one after the extmarks responsible of start/end of annotation
cursor.go_next_extmark = function()
local extm_list = vim.api.nvim_buf_get_extmarks(0, neogen_ns, 0, -1, {})
local position = current_position + 1
table.sort(extm_list, function(a, b)
return a[1] < b[1]
end)
if #extm_list ~= 2 then
local pos = { extm_list[position][2] + 1, extm_list[position][3] }
vim.api.nvim_win_set_cursor(0, pos)
current_position = current_position + 1
return true
else
return false
end
end
--- Goes to next extmark and start insert mode.
--- If `opts.first_time` is supplied, will try to go to normal mode before going to extmark
--- @param opts table
cursor.jump = function(opts)
opts = opts or {}
-- This is weird, the first time nvim goes to insert is not the same as when i'm already on insert mode
-- that's why i put a first_time flag
if opts.first_time then
vim.api.nvim_command("startinsert")
end
if cursor.go_next_extmark() then
vim.api.nvim_command("startinsert")
end
end
cursor.jump_prev = function()
local marks = vim.api.nvim_buf_get_extmarks(0, neogen_ns, 0, -1, {})
table.sort(marks, function(a, b)
return a[1] < b[1]
end)
if #marks == 2 then
return false
end
local position = current_position - 1
local pos = { marks[position][2] + 1, marks[position][3] }
vim.api.nvim_win_set_cursor(0, pos)
current_position = current_position - 1
return true
end
--- Delete all active extmarks
cursor.del_extmarks = function()
local extmarks = vim.api.nvim_buf_get_extmarks(0, neogen_ns, 0, -1, {})
for _, v in pairs(extmarks) do
vim.api.nvim_buf_del_extmark(0, neogen_ns, v[1])
end
end
--- Checks if there are still possible jump positions to perform
--- Verifies if the cursor is in the last annotated part
cursor.jumpable = function(reverse)
local extm_list = vim.api.nvim_buf_get_extmarks(0, neogen_ns, 0, -1, {})
if #extm_list == 0 then
return false
end
local cursor = vim.api.nvim_win_get_cursor(0)
if cursor[1] > extm_list[#extm_list][2] or cursor[1] < extm_list[1][2] then
return false
end
-- We arrive at the end, we can't jump anymore
if current_position == #extm_list then
return false
end
if reverse == -1 then
-- Check first boundaries
if current_position == 2 then
return false
end
end
if #extm_list > 2 then
return true
else
return false
end
end
return cursor

View File

@@ -16,7 +16,7 @@ return {
local get_text = function(node) local get_text = function(node)
return ts_utils.get_node_text(node)[1] return ts_utils.get_node_text(node)[1]
end end
if opts.type == true then if opts.type then
result[k] = vim.tbl_map(get_type, v) result[k] = vim.tbl_map(get_type, v)
else else
result[k] = vim.tbl_map(get_text, v) result[k] = vim.tbl_map(get_text, v)

View File

@@ -1,6 +1,7 @@
local config = require("neogen.config")
return { return {
notify = function(msg, log_level) notify = function(msg, log_level)
vim.notify(msg, log_level, { title = "Neogen" }) vim.notify(msg, log_level, {title = "Neogen"})
end, end,
--- Generates a list of possible types in the current language --- Generates a list of possible types in the current language
@@ -10,7 +11,7 @@ return {
return {} return {}
end end
local language = neogen.configuration.languages[vim.bo.filetype] local language = config.get().languages[vim.bo.filetype]
if not language or not language.parent then if not language or not language.parent then
return {} return {}
@@ -18,19 +19,8 @@ return {
return vim.tbl_keys(language.parent) return vim.tbl_keys(language.parent)
end, end,
split = function(s, sep, plain)
switch_language = function() return vim.fn.has("nvim-0.6") == 1 and vim.split(s, sep, {plain = plain}) or
local filetype = vim.bo.filetype vim.split(s, sep, plain)
local ok, ft_configuration = pcall(require, "neogen.configurations." .. filetype)
if not ok then
return
end end
neogen.configuration.languages[filetype] = vim.tbl_deep_extend(
"keep",
neogen.user_configuration.languages and neogen.user_configuration.languages[filetype] or {},
ft_configuration
)
end,
} }

View File

@@ -1,22 +1,23 @@
local helpers = require("neogen.utilities.helpers")
return { return {
--- Get a list of child nodes that match the provided node name --- Get a list of child nodes that match the provided node name
--- @param _ any --- @param _ any
--- @param parent userdata the parent's node --- @param parent userdata the parent's node
--- @param node_name string|nil the node type to search for (if multiple childrens, separate each one with "|") --- @param node_type string|nil the node type to search for (if multiple childrens, separate each one with "|")
--- @return table a table of nodes that matched the name --- @return table a table of nodes that matched the name
matching_child_nodes = function(_, parent, node_name) matching_child_nodes = function(_, parent, node_type)
local results = {} local results = {}
-- Return all nodes if there is no node name -- Return all nodes if there is no node name
if node_name == nil then if not node_type then
for child in parent:iter_children() do for child in parent:iter_children() do
if child:named() then if child:named() then
table.insert(results, child) table.insert(results, child)
end end
end end
else else
local split = vim.split(node_name, "|", true) local types = helpers.split(node_type, "|", true)
for child in parent:iter_children() do for child in parent:iter_children() do
if vim.tbl_contains(split, child:type()) then if vim.tbl_contains(types, child:type()) then
table.insert(results, child) table.insert(results, child)
end end
end end
@@ -45,8 +46,7 @@ return {
break break
end end
local found = self:recursive_find(child, node_name, { results = results }) self:recursive_find(child, node_name, {results = results})
vim.tbl_deep_extend("keep", results, found)
end end
return results return results
@@ -63,41 +63,36 @@ return {
result = result or {} result = result or {}
for _, subtree in pairs(tree) do for _, subtree in pairs(tree) do
if subtree.retrieve and not vim.tbl_contains({ "all", "first" }, subtree.retrieve) then assert(not subtree.retrieve or vim.tbl_contains({"all", "first"}, subtree.retrieve),
assert(false, "Supported nodes matching: all|first") "Supported nodes matching: all|first")
return
end
-- Match all child nodes of the parent node -- Match all child nodes of the parent node
local matched = self:matching_child_nodes(parent, subtree.node_type) local matched = self:matching_child_nodes(parent, subtree.node_type)
-- Only keep the node with custom position -- Only keep the node with custom position
if subtree.retrieve == nil then if not subtree.retrieve then
if type(subtree.position) == "number" then assert(type(subtree.position) == "number",
matched = { matched[subtree.position] } "please require position if retrieve is nil")
else matched = {matched[subtree.position]}
assert(false, "please require position if retrieve is nil")
end
end end
if subtree.recursive then if subtree.recursive then
local first = subtree.retrieve == "first" local first = subtree.retrieve == "first"
matched = self:recursive_find(parent, subtree.node_type, { first = first }) matched = self:recursive_find(parent, subtree.node_type, {first = first})
end end
for _, child in pairs(matched) do for _, child in pairs(matched) do
if subtree.extract == true then if subtree.extract then
local name = subtree.as and subtree.as or (subtree.node_type or "_") local name = subtree.as and subtree.as or (subtree.node_type or "_")
if result[name] == nil then if not result[name] then
result[name] = {} result[name] = {}
end end
table.insert(result[name], child) table.insert(result[name], child)
else else
local nodes = self:matching_nodes_from(child, subtree.subtree, result) self:matching_nodes_from(child, subtree.subtree, result)
result = vim.tbl_deep_extend("keep", result, nodes)
end end
end end
end end
return result return result
end, end
} }

View File

@@ -4,4 +4,4 @@ if _G.MiniDoc == nil then
minidoc.setup() minidoc.setup()
end end
minidoc.generate({ "lua/neogen/init.lua", "lua/neogen/utilities/template.lua" }, nil, nil) minidoc.generate({ "lua/neogen/init.lua", "lua/neogen/template.lua" }, nil, nil)