Need some advice for my first plugin

Hello everyone!
One problem I’ve been having since I switched to the builtin lsp coc a few months ago is that I haven’t found a way to get the diagnostics only on my current line. Today I thought that since I started to learn Lua I could try to do it myself with a little bit of tweaking. I know it doesn’t bring enough features to be a plugin by itself, it’s more like a small functionality in a config but I thought it would be a good opportunity to make my first plugin anyway. I don’t plan to post a maintained version at all, it’s just for my configuration and my “curiosity”. Since I’ve never made a plugin I have absolutely no idea what the project structure should be, should I split it into several files etc… I would also be interested in opinions about the code itself! Please be kind, I begin (:
Just a quick look is more than enough, thank you very much for your time and have a nice day!

--[[

To configure the feature, simply call the `require "vtcurr".setup` function this way:

vim.diagnostic.config({
    ...
    virtual_text = {
        severity = require('vtcurr').setup {
            min = vim.diagnostic.severity.ERROR
        },
    },
    ...
})

]]

local VT = {}

local ns = nvim.create_namespace("VT")

VT.sev = {}

VT.setup = function(config)

    local sev = vim.diagnostic.severity
    local max = config.max
    local min = config.min

    for k, v in pairs(sev) do

        if type(v) == "string" or #k <= 1 then
            goto continue
        end

        if (min and v > min) or (max and v < max) then
            if not VT.sev.max or
                v < sev[VT.sev.max] then
                VT.sev.max = k
            end
            if not VT.sev.min or
                v > sev[VT.sev.min] then
                VT.sev.min = k
            end
        end

        ::continue::

    end

    -- I have absolutely no idea which events I should use
    vim.api.nvim_create_autocmd(
        { "CursorMoved", "CursorHold",
            "CursorHoldI", "InsertEnter",
            "InsertLeave", "BufEnter" }, {
        callback = function()
            local ln = vim.fn.line('.') - 1
            VT.clear()

            if VT.cond(ln) then
                VT.print(ln, VT.get(ln))
            end

        end
    })

    VT.lsp = config

    return config
end

-- @description allows to display a virtual line which
-- content and line are given
--
-- @param ln -> int: line 0-indexed
-- @param content -> string: content of the virtuel text.
VT.print = function(ln, content)

    local col = string.len(nvim.buf_get_lines(
        0, ln, ln + 1, false
    )[1])

    nvim.buf_set_extmark(0, ns, ln, col, {
        virt_text = VT.fmt(content)
    })

end

-- @description retrieves all diagnostics attached to
-- the current buffer and on the line given as argument
--
-- @param ln -> int: line 0-indexed
VT.get = function(ln)
    return vim.diagnostic.get(0, {
        lnum = ln,
        severity = VT.sev
    })
end

-- @description Clear all virtual texts previously
-- displayed by the VT.print() function
-- @see VT.print()
VT.clear = function()

    local buflst = vim.tbl_filter(function(nr)
        return nvim.buf_is_loaded(nr)
    end, nvim.list_bufs())

    for _, bufnr in ipairs(buflst) do
        nvim.buf_clear_namespace(bufnr, ns, 0, -1)
    end
end

-- @description Format a diagnostic list into a
-- string that can be displayed as a virtual line.
--
-- @param diagnostic -> lst: list of all the
-- diagnostic to print
VT.fmt = function(diagnostics)
    local colors = {
        "Error",
        "Warn",
        "Info",
        "Hint"
    }

    local content = {}
    local max

    for c, di in pairs(diagnostics) do

        if max == nil or di.severity >= max then
            max = di.severity
        end

        table.insert(content, {
            string.rep(c == 1 and " " or "", 4) .. "■",
            "Diagnostic" .. colors[di.severity]
        })
    end

    table.insert(
        content,
        { " " .. diagnostics[1].message, "Diagnostic" .. colors[max] }
    )

    return content
end

-- @description Conditions the display of a virtual line on the
-- given line of the current buffer. If no virtual line is already
-- displayed of a severity not supported by vtcurr and a line
-- of a supported severity is to be displayed then return true.
VT.cond = function(ln)
    return #vim.diagnostic.get(0, {
        lnum = ln,
        severity = VT.lsp
    }) == 0 and #VT.get(ln) > 0
end

return VT

The project structure should be whatever is most convenient to you. Do you want to always load it on start? Put it in a file under plugin. Do you want to use it as a library? Put in under a lua subdirectory. Do you want both? Write a Lua library in a lua, then create a script under plugin which requires the library and calls a function or something. It is entirely up to you.

Looking at your code I think the library approach would be best, that way users can decide when and where they want to load your code. You can also provide Vim script bindings in an auto-load file like autoload/vt.vim, that way users are not forced into Lua. The Vim script functions would just be wrappers around the Lua functions, no duplicated logic.

Very important: make sure to namespace your Lua library files. If you have some utility file don’t save it as lua/util.lua, save it as lua/vt/util.lua, or else your util file will step on some other util file. There is no need to namespace other files, i.e. you can write plugin/util.lua instead of plugin/vt/util.lua because those files cannot be manually required.

A couple of notes about your code:

  • goto is not part of Lua 5.1, it is a Lua 5.2 feature which LuaJIT implements as an extension to Lua 5.1; if your Neovim is not compiled with LuaJIT the code will not work. The goto statement is very useful here and I don’t have a better idea, it’s just something to keep in mind
  • string.len will give you the length of a string in bytes, not characters. If you want to limit yourself to ASCII this will be fine, but once you have a multibyte characters the result will be too large. Use vim.fn.strchars instead
  • Do you really want to export functions like VT.cond? If not then you should declare them as local functions, keeping implementation details private is always a good idea