diff --git a/README.md b/README.md index c7ceef6..95e9b64 100644 --- a/README.md +++ b/README.md @@ -112,40 +112,31 @@ Or, if you want to use a key that's already used for completion purposes, take a 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 = { - [""] = cmp.mapping(function(fallback) - if neogen.jumpable() then - vim.fn.feedkeys(t("lua require('neogen').jump_next()"), "") - else - fallback() - end - end, { - "i", - "s", - }), - [""] = cmp.mapping(function(fallback) - if neogen.jumpable(-1) then - vim.fn.feedkeys(t("lua require('neogen').jump_prev()"), "") - else - fallback() - end - end, { - "i", - "s", - }), + [""] = cmp.mapping(function(fallback) + if require('neogen').jumpable() then + require('neogen').jump_next() + else + fallback() + end + end, { + "i", + "s", + }), + [""] = cmp.mapping(function(fallback) + if require('neogen').jumpable(true) then + require('neogen').jump_prev() + else + fallback() + end + end, { + "i", + "s", + }), }, ... } @@ -169,14 +160,14 @@ If you're not satisfied with the default configuration for a language, you can c ```lua 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 - } - }, - ... + 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 + } + }, + ... } } ``` diff --git a/doc/neogen.txt b/doc/neogen.txt index 5017973..ecea7ff 100644 --- a/doc/neogen.txt +++ b/doc/neogen.txt @@ -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 input_after_comment = true, - -- Symbol to find for jumping cursor in template - jump_text = "$1", - -- Configuration for default languages - languages = {}, + languages = {} } < # 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`({opts}) @@ -366,4 +360,4 @@ If not specified, will use this line for all types. {required} `(string)` If specified, is used in if the first field of the table is a `table` (example above) - vim:tw=78:ts=8:noet:ft=help:norl: \ No newline at end of file + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/lua/neogen/config.lua b/lua/neogen/config.lua new file mode 100644 index 0000000..62e9ac4 --- /dev/null +++ b/lua/neogen/config.lua @@ -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 diff --git a/lua/neogen/configurations/c.lua b/lua/neogen/configurations/c.lua index 152cccb..27fd014 100644 --- a/lua/neogen/configurations/c.lua +++ b/lua/neogen/configurations/c.lua @@ -1,7 +1,7 @@ local extractors = require("neogen.utilities.extractors") local nodes_utils = require("neogen.utilities.nodes") 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 c_params = { @@ -149,14 +149,14 @@ local c_config = { return nil end - if node_info.current == nil then + if not node_info.current then return result end -- if the function happens to be a function template we want to place -- the annotation before the template statement and extract the -- template parameters names as well - if node_info.current:parent() == nil then + if not node_info.current:parent() then return result end if node_info.current:parent():type() == "template_declaration" then @@ -165,10 +165,6 @@ local c_config = { return result end, - -- Use default granulator and generator - granulator = nil, - generator = nil, - template = template:add_default_annotation("doxygen"), } diff --git a/lua/neogen/configurations/cs.lua b/lua/neogen/configurations/cs.lua index 3e3b887..d1497de 100644 --- a/lua/neogen/configurations/cs.lua +++ b/lua/neogen/configurations/cs.lua @@ -1,6 +1,6 @@ local extractors = require("neogen.utilities.extractors") 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 return { diff --git a/lua/neogen/configurations/go.lua b/lua/neogen/configurations/go.lua index 4efd4e4..6d633b6 100644 --- a/lua/neogen/configurations/go.lua +++ b/lua/neogen/configurations/go.lua @@ -1,4 +1,4 @@ -local template = require("neogen.utilities.template") +local template = require("neogen.template") return { parent = { diff --git a/lua/neogen/configurations/java.lua b/lua/neogen/configurations/java.lua index 83ae439..aa51680 100644 --- a/lua/neogen/configurations/java.lua +++ b/lua/neogen/configurations/java.lua @@ -1,6 +1,6 @@ local extractors = require("neogen.utilities.extractors") local nodes_utils = require("neogen.utilities.nodes") -local template = require("neogen.utilities.template") +local template = require("neogen.template") local function_tree = { { diff --git a/lua/neogen/configurations/javascript.lua b/lua/neogen/configurations/javascript.lua index d0871c7..d04a5c0 100644 --- a/lua/neogen/configurations/javascript.lua +++ b/lua/neogen/configurations/javascript.lua @@ -1,7 +1,7 @@ local i = require("neogen.types.template").item local extractors = require("neogen.utilities.extractors") local nodes_utils = require("neogen.utilities.nodes") -local template = require("neogen.utilities.template") +local template = require("neogen.template") local function_tree = { { diff --git a/lua/neogen/configurations/lua.lua b/lua/neogen/configurations/lua.lua index b2b5f7b..d04adcb 100644 --- a/lua/neogen/configurations/lua.lua +++ b/lua/neogen/configurations/lua.lua @@ -1,7 +1,7 @@ local extractors = require("neogen.utilities.extractors") local i = require("neogen.types.template").item local nodes_utils = require("neogen.utilities.nodes") -local template = require("neogen.utilities.template") +local template = require("neogen.template") local function_extractor = function(node, type) if not vim.tbl_contains({ "local", "function" }, type) then @@ -145,9 +145,5 @@ return { -- Custom lua locator that escapes from comments 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"), } diff --git a/lua/neogen/configurations/php.lua b/lua/neogen/configurations/php.lua index c73f172..ebb7950 100644 --- a/lua/neogen/configurations/php.lua +++ b/lua/neogen/configurations/php.lua @@ -1,6 +1,6 @@ local extractors = require("neogen.utilities.extractors") 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 return { diff --git a/lua/neogen/configurations/python.lua b/lua/neogen/configurations/python.lua index 934cafe..944ae42 100644 --- a/lua/neogen/configurations/python.lua +++ b/lua/neogen/configurations/python.lua @@ -2,7 +2,7 @@ local ts_utils = require("nvim-treesitter.ts_utils") local nodes_utils = require("neogen.utilities.nodes") local extractors = require("neogen.utilities.extractors") 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 parent = { diff --git a/lua/neogen/configurations/rust.lua b/lua/neogen/configurations/rust.lua index 0056963..4922c1e 100644 --- a/lua/neogen/configurations/rust.lua +++ b/lua/neogen/configurations/rust.lua @@ -1,7 +1,7 @@ local extractors = require("neogen.utilities.extractors") local i = require("neogen.types.template").item local nodes_utils = require("neogen.utilities.nodes") -local template = require("neogen.utilities.template") +local template = require("neogen.template") return { parent = { diff --git a/lua/neogen/configurations/typescript.lua b/lua/neogen/configurations/typescript.lua index b528af8..0beca7f 100644 --- a/lua/neogen/configurations/typescript.lua +++ b/lua/neogen/configurations/typescript.lua @@ -1,7 +1,7 @@ local extractors = require("neogen.utilities.extractors") local nodes_utils = require("neogen.utilities.nodes") 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 results = parameters and {} or nil diff --git a/lua/neogen/generator.lua b/lua/neogen/generator.lua new file mode 100644 index 0000000..1ec1706 --- /dev/null +++ b/lua/neogen/generator.lua @@ -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 +}) + diff --git a/lua/neogen/generators/default.lua b/lua/neogen/generators/default.lua deleted file mode 100644 index 27e06e0..0000000 --- a/lua/neogen/generators/default.lua +++ /dev/null @@ -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 diff --git a/lua/neogen/granulator.lua b/lua/neogen/granulator.lua new file mode 100644 index 0000000..616ad37 --- /dev/null +++ b/lua/neogen/granulator.lua @@ -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 diff --git a/lua/neogen/granulators/default.lua b/lua/neogen/granulators/default.lua deleted file mode 100644 index ebeb8c7..0000000 --- a/lua/neogen/granulators/default.lua +++ /dev/null @@ -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 diff --git a/lua/neogen/init.lua b/lua/neogen/init.lua index be315c6..d368eba 100644 --- a/lua/neogen/init.lua +++ b/lua/neogen/init.lua @@ -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: ---@toc ---@text @@ -22,22 +19,15 @@ assert(ok, "neogen requires nvim-treesitter to operate :(") --- - Written in lua (and uses Tree-sitter) ---@tag neogen ---@toc_entry Neogen's purpose - -- Requires =================================================================== - local neogen = {} +local conf +local config = require("neogen.config") local helpers = require("neogen.utilities.helpers") +local mark = require("neogen.mark") 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 setup @@ -47,20 +37,10 @@ local autocmd = require("neogen.utilities.autocmd") ---@usage `require('neogen').setup({})` (replace `{}` with your `config` table) ---@toc_entry The setup function neogen.setup = function(opts) - -- Stores the user configuration globally so that we keep his configs when switching languages - neogen.user_configuration = opts or {} - - neogen.configuration = vim.tbl_deep_extend("keep", neogen.user_configuration, neogen.configuration) - - if neogen.configuration.enabled == true then + conf = config.setup(neogen.configuration, opts) + if conf.enabled then neogen.generate_command() end - - -- Export module - _G.neogen = neogen - - -- Force configuring current language again when doing `setup` call. - helpers.switch_language() end --- 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 ---@tag neogen-configuration neogen.configuration = { @@ -128,9 +106,6 @@ neogen.configuration = { -- Go to annotation after insertion, and change to insert mode input_after_comment = true, - -- Symbol to find for jumping cursor in template - jump_text = "$1", - -- Configuration for default languages languages = {}, } @@ -149,128 +124,12 @@ neogen.configuration = { --- Currently supported: `func`, `class`, `type`, `file` ---@toc_entry Generate annotations neogen.generate = function(opts) - opts = opts or {} - opts.type = (opts.type == nil or opts.type == "") and "func" or opts.type -- Default type - - if not neogen.configuration.enabled then + if not conf.enabled then notify("Neogen not enabled. Please enable it.", vim.log.levels.WARN) return end - if vim.bo.filetype == "" then - 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 + require("neogen.generator")(vim.bo.filetype, opts and opts.type) end -- Expose more API ============================================================ @@ -279,18 +138,15 @@ end neogen.match_commands = helpers.match_commands --- Get a template for a particular filetype ----@param filetype string +---@param filetype? string ---@return neogen.TemplateConfig|nil neogen.get_template = function(filetype) - if not neogen.configuration.languages[filetype] then - return + local template + 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 - - if not neogen.configuration.languages[filetype].template then - return - end - - return neogen.configuration.languages[filetype].template + return template end -- Required for use with completion engine ===================================== @@ -298,24 +154,20 @@ end --- Jumps to the next cursor template position ---@private function neogen.jump_next() - if neogen.jumpable() then - cursor.jump() - end + mark:jump() end --- Jumps to the next cursor template position ---@private function neogen.jump_prev() - if cursor.jumpable(-1) then - cursor.jump_prev() - end + mark:jump(true) end --- 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 function neogen.jumpable(reverse) - return cursor.jumpable(reverse) + return mark:jumpable(reverse == -1 or reverse == true) end -- Command and autocommands ==================================================== @@ -323,22 +175,11 @@ end --- Generates the `:Neogen` command, which calls `neogen.generate()` ---@private function neogen.generate_command() - vim.api.nvim_command( - 'command! -nargs=? -complete=customlist,v:lua.neogen.match_commands -range -bar Neogen lua require("neogen").generate({ type = })' - ) + vim.cmd([[ + command! -nargs=? -complete=customlist,v:lua.require'neogen'.match_commands -range -bar Neogen lua require("neogen").generate({ type = }) + ]]) 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 --- --- * Want to add a new language? diff --git a/lua/neogen/locators/default.lua b/lua/neogen/locators/default.lua index b4ccf0b..14edc7b 100644 --- a/lua/neogen/locators/default.lua +++ b/lua/neogen/locators/default.lua @@ -1,12 +1,12 @@ ---- @class Neogen.node_info ---- @field current userdata the current node from cursor ---- @field root? userdata the root node +---@class Neogen.node_info +---@field current userdata the current node from cursor +---@field root? userdata the root 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 ---- @param node_info Neogen.node_info a node informations ---- @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 +---@param node_info Neogen.node_info a node informations +---@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 function(node_info, nodes_to_match) if not node_info.current then if vim.tbl_contains(nodes_to_match, node_info.root:type()) then diff --git a/lua/neogen/mark.lua b/lua/neogen/mark.lua new file mode 100644 index 0000000..ab02013 --- /dev/null +++ b/lua/neogen/mark.lua @@ -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 diff --git a/lua/neogen/utilities/template.lua b/lua/neogen/template.lua similarity index 100% rename from lua/neogen/utilities/template.lua rename to lua/neogen/template.lua diff --git a/lua/neogen/utilities/autocmd.lua b/lua/neogen/utilities/autocmd.lua index cd4e2f8..218a34f 100644 --- a/lua/neogen/utilities/autocmd.lua +++ b/lua/neogen/utilities/autocmd.lua @@ -23,7 +23,7 @@ local autocmd = {} autocmd.events = {} ----Subscribe autocmd +--- Subscribe autocmd ---@param event string ---@param callback function ---@return function @@ -40,7 +40,7 @@ autocmd.subscribe = function(event, callback) end end ----Emit autocmd +--- Emit autocmd ---@param event string autocmd.emit = function(event) autocmd.events[event] = autocmd.events[event] or {} diff --git a/lua/neogen/utilities/cursor.lua b/lua/neogen/utilities/cursor.lua deleted file mode 100644 index 773cb0c..0000000 --- a/lua/neogen/utilities/cursor.lua +++ /dev/null @@ -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 diff --git a/lua/neogen/utilities/extractors.lua b/lua/neogen/utilities/extractors.lua index e65da01..b1b6e01 100644 --- a/lua/neogen/utilities/extractors.lua +++ b/lua/neogen/utilities/extractors.lua @@ -16,7 +16,7 @@ return { local get_text = function(node) return ts_utils.get_node_text(node)[1] end - if opts.type == true then + if opts.type then result[k] = vim.tbl_map(get_type, v) else result[k] = vim.tbl_map(get_text, v) diff --git a/lua/neogen/utilities/helpers.lua b/lua/neogen/utilities/helpers.lua index ca426cf..5a66fb0 100644 --- a/lua/neogen/utilities/helpers.lua +++ b/lua/neogen/utilities/helpers.lua @@ -1,6 +1,7 @@ +local config = require("neogen.config") return { notify = function(msg, log_level) - vim.notify(msg, log_level, { title = "Neogen" }) + vim.notify(msg, log_level, {title = "Neogen"}) end, --- Generates a list of possible types in the current language @@ -10,7 +11,7 @@ return { return {} 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 return {} @@ -18,19 +19,8 @@ return { return vim.tbl_keys(language.parent) end, - - switch_language = function() - local filetype = vim.bo.filetype - local ok, ft_configuration = pcall(require, "neogen.configurations." .. filetype) - - if not ok then - return - end - - neogen.configuration.languages[filetype] = vim.tbl_deep_extend( - "keep", - neogen.user_configuration.languages and neogen.user_configuration.languages[filetype] or {}, - ft_configuration - ) - end, + split = function(s, sep, plain) + return vim.fn.has("nvim-0.6") == 1 and vim.split(s, sep, {plain = plain}) or + vim.split(s, sep, plain) + end } diff --git a/lua/neogen/utilities/nodes.lua b/lua/neogen/utilities/nodes.lua index 8c45568..448eb02 100644 --- a/lua/neogen/utilities/nodes.lua +++ b/lua/neogen/utilities/nodes.lua @@ -1,22 +1,23 @@ +local helpers = require("neogen.utilities.helpers") return { --- Get a list of child nodes that match the provided node name --- @param _ any --- @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 - matching_child_nodes = function(_, parent, node_name) + matching_child_nodes = function(_, parent, node_type) local results = {} -- 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 if child:named() then table.insert(results, child) end end else - local split = vim.split(node_name, "|", true) + local types = helpers.split(node_type, "|", true) 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) end end @@ -45,8 +46,7 @@ return { break end - local found = self:recursive_find(child, node_name, { results = results }) - vim.tbl_deep_extend("keep", results, found) + self:recursive_find(child, node_name, {results = results}) end return results @@ -63,41 +63,36 @@ return { result = result or {} for _, subtree in pairs(tree) do - if subtree.retrieve and not vim.tbl_contains({ "all", "first" }, subtree.retrieve) then - assert(false, "Supported nodes matching: all|first") - return - end + assert(not subtree.retrieve or vim.tbl_contains({"all", "first"}, subtree.retrieve), + "Supported nodes matching: all|first") -- Match all child nodes of the parent node local matched = self:matching_child_nodes(parent, subtree.node_type) -- Only keep the node with custom position - if subtree.retrieve == nil then - if type(subtree.position) == "number" then - matched = { matched[subtree.position] } - else - assert(false, "please require position if retrieve is nil") - end + if not subtree.retrieve then + assert(type(subtree.position) == "number", + "please require position if retrieve is nil") + matched = {matched[subtree.position]} end if subtree.recursive then 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 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 "_") - if result[name] == nil then + if not result[name] then result[name] = {} end table.insert(result[name], child) else - local nodes = self:matching_nodes_from(child, subtree.subtree, result) - result = vim.tbl_deep_extend("keep", result, nodes) + self:matching_nodes_from(child, subtree.subtree, result) end end end return result - end, + end } diff --git a/scripts/minidoc.lua b/scripts/minidoc.lua index e19435b..cdc1b0e 100644 --- a/scripts/minidoc.lua +++ b/scripts/minidoc.lua @@ -4,4 +4,4 @@ if _G.MiniDoc == nil then minidoc.setup() 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)