- I subscribed to the BufEnter autocmd to switch language configs. Only previously opened languages are kept inside the languages table
326 lines
11 KiB
Lua
326 lines
11 KiB
Lua
local ok, ts_utils = pcall(require, "nvim-treesitter.ts_utils")
|
|
assert(ok, "neogen requires nvim-treesitter to operate :(")
|
|
|
|
--- What is Neogen ?
|
|
---
|
|
--- # Abstract~
|
|
---
|
|
--- Neogen is an extensible and extremely configurable annotation generator for your favorite languages
|
|
---
|
|
--- Want to know what the supported languages are ?
|
|
--- Check out the up-to-date readme section: https://github.com/danymat/neogen#supported-languages
|
|
---
|
|
--- # Concept~
|
|
---
|
|
--- - Create annotations with one keybind, and jump your cursor in the inserted annotation
|
|
--- - Defaults for multiple languages and annotation conventions
|
|
--- - Extremely customizable and extensible
|
|
--- - Written in lua (and uses Tree-sitter)
|
|
---@tag neogen
|
|
|
|
-- Requires ===================================================================
|
|
|
|
local neogen = {}
|
|
|
|
local helpers = require("neogen.utilities.helpers")
|
|
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
|
|
---
|
|
---@param opts table Config table (see |neogen.configuration|)
|
|
---
|
|
---@usage `require('neogen').setup({})` (replace `{}` with your `config` table)
|
|
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
|
|
neogen.generate_command()
|
|
end
|
|
|
|
-- Export module
|
|
_G.neogen = neogen
|
|
end
|
|
|
|
--- Neogen Usage
|
|
---
|
|
--- Neogen will use Treesitter parsing to properly generate annotations.
|
|
---
|
|
--- The basic idea is that Neogen will generate annotation to the type you're in.
|
|
--- For example, if you have a csharp function like (note the cursor position):
|
|
---
|
|
--- >
|
|
--- public class HelloWorld
|
|
--- {
|
|
--- public static void Main(string[] args)
|
|
--- {
|
|
--- # CURSOR HERE
|
|
--- Console.WriteLine("Hello world!");
|
|
--- return true;
|
|
--- }
|
|
---
|
|
--- public int someMethod(string str, ref int nm, void* ptr) { return 1; }
|
|
---
|
|
--- }
|
|
--- <
|
|
---
|
|
--- and you call `:Neogen class`, it will generate the annotation for the upper class:
|
|
---
|
|
--- >
|
|
--- /// <summary>
|
|
--- /// ...
|
|
--- /// </summary>
|
|
--- public class HelloWorld
|
|
--- {
|
|
--- <
|
|
---
|
|
--- Currently supported types are `func`, `class`, `type`, `file`.
|
|
--- Check out the up-to-date readme section: https://github.com/danymat/neogen#supported-languages
|
|
--- To know the supported types for a certain language
|
|
---
|
|
--- NOTE: calling `:Neogen` without any type is the same as `:Neogen func`
|
|
---@tag neogen.usage
|
|
|
|
--- # Basic configurations~
|
|
---
|
|
--- Neogen provides those defaults, and you can change them to suit your needs
|
|
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
|
|
---@test # Notes~
|
|
---
|
|
--- - to configure a language, just add your configurations in the `languages` table
|
|
--- For example, for the `lua` lang:
|
|
--- >
|
|
--- languages = {
|
|
--- lua = { -- Configuration here }
|
|
--- }
|
|
--- <
|
|
---
|
|
--- - `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.configuration = {
|
|
-- Enables Neogen capabilities
|
|
enabled = true,
|
|
|
|
-- 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 = {},
|
|
}
|
|
--minidoc_afterlines_end
|
|
|
|
-- Basic API ===================================================================
|
|
|
|
--- The only function required to use Neogen.
|
|
---
|
|
--- It'll try to find the first parent that matches a certain type.
|
|
--- For example, if you are inside a function, and called `generate({ type = "func" })`,
|
|
--- Neogen will go until the start of the function and start annotating for you.
|
|
---
|
|
---@param opts table Options to change default behaviour of generation.
|
|
--- - {opts.type} `(string?, default: "func")` Which type we are trying to use for generating annotations.
|
|
--- Currently supported: `func`, `class`, `type`, `file`
|
|
---@tag test.generator
|
|
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
|
|
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
|
|
end
|
|
|
|
-- Expose more API ============================================================
|
|
|
|
-- Expose match_commands for `:Neogen` completion
|
|
neogen.match_commands = helpers.match_commands
|
|
|
|
-- Required for use with completion engine =====================================
|
|
|
|
--- Jumps to the next cursor template position
|
|
---@private
|
|
function neogen.jump_next()
|
|
cursor.jump()
|
|
end
|
|
|
|
--- Jumps to the next cursor template position
|
|
---@private
|
|
function neogen.jump_prev()
|
|
cursor.jump_prev()
|
|
end
|
|
|
|
--- Checks if the cursor can jump backwards or forwards
|
|
--- @param reverse number? if `-1`, will try to see if can be jumped backwards
|
|
---@private
|
|
function neogen.jumpable(reverse)
|
|
return cursor.jumpable(reverse)
|
|
end
|
|
|
|
-- Command and autocommands ====================================================
|
|
|
|
--- 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 = <q-args>})'
|
|
)
|
|
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?
|
|
--- 1. Using the defaults to generate a new language support:
|
|
--- https://github.com/danymat/neogen/blob/main/docs/adding-languages.md
|
|
--- 2. (advanced) Only if the defaults aren't enough, please see here:
|
|
--- https://github.com/danymat/neogen/blob/main/docs/advanced-integration.md
|
|
--- * Want to contribute to an existing language?
|
|
--- I guess you can still read the previous links, as they have some valuable knowledge inside.
|
|
--- You can go and directly open/edit the configuration file relative to the language you want to contribute to.
|
|
---
|
|
---Feel free to submit a PR, I will be happy to help you !
|
|
---@tag neogen.develop
|
|
|
|
return neogen
|