Skip to content

Added snippet_engine parameter#202

Open
ColinKennedy wants to merge 1 commit intodanymat:mainfrom
ColinKennedy:add_snippet_engine_parameter
Open

Added snippet_engine parameter#202
ColinKennedy wants to merge 1 commit intodanymat:mainfrom
ColinKennedy:add_snippet_engine_parameter

Conversation

@ColinKennedy
Copy link
Copy Markdown
Contributor

@ColinKennedy ColinKennedy commented Sep 2, 2024

A (sort of) continuation of #201

I integrate Neogen + LuaSnip into one with this snippet:

Click to expand
--- Create LuaSnip snippets that can auto-expand Neogen's snippet engine.
---
--- @module 'my_custom.snippets._python_docstring'
---

local snippet_helper = require("my_custom.utilities.snippet_helper")
local luasnip = require("luasnip")
local snippet = luasnip.s
local dynamicNode = require("luasnip.nodes.dynamicNode").D
local snippetNode = require("luasnip.nodes.snippet").SN

--- Create a dynamic LuaSnip snippet-node whose contents are created by Neogen.
---
--- @param section string The Neogen section name to create a LuaSnip snippet.
--- @return LuaSnip.DynamicNode # The created node.
---
local function _make_section_snippet_node(section)
    return dynamicNode(
        1,
        function(args)
            local neogen = require("neogen")

            local lines = neogen.generate(
                -- TODO: Provide an explicit snippet engine (once there's an
                -- argument for it).
                {return_snippet = true, sections = {section}, snippet_engine = "luasnip"}
            )

            local nodes = luasnip.parser.parse_snippet(
                nil,
                table.concat(lines, "\n"),
                { trim_empty = false, dedent = true }
            )

            return snippetNode(nil, nodes)
        end
    )
end

--- Create a LuaSnip snippet for some `section`. Run it when `trigger` is found.
---
--- @param trigger string
---     A word that LuaSnip uses to decide when the snippet should run. e.g. `"Args:"`.
--- @param section string
---     The parts of a docstring that Neogen needs to generate.
--- @return LuaSnip.Snippet
---     The generated auto-complete snippet.
---
local function _make_section_snippet(trigger, section)
    return snippet(
        {
            trig=trigger,
            docstring=string.format(
                'Auto-fill a docstring\'s "%s" section, using Neogen',
                trigger
            ),
        },
        {
            _make_section_snippet_node(section)
        },
        {
            show_condition = function()
                return (
                    snippet_helper.in_docstring()
                    and snippet_helper.is_source_beginning(trigger)
                )
            end
        }
    )
end

return {
    _make_section_snippet("Args:", "parameter"),
    _make_section_snippet("Raises:", "throw"),
    _make_section_snippet("Returns:", "return"),
    _make_section_snippet("Yields:", "yield"),
}

(This later gets included to `luasnip.add_snippets("python", those_snippets_above)

With this, I can just write docstrings naturally like Args: and Neogen auto-expands the arguments for me.

The end result looks like this:

2024-09-02.16-02-25.mp4
2024-09-02.16-02-39.mp4

(Sorry the recordings are a bit messed up)

Anyway being able to explicitly ask for the snippet engine means that a user can default to a different snippet engine in their configuration but run a different engine on-demand. This is useful as someone who wants to move to native LSP snippets someday.

Added missing version information
@jonathf
Copy link
Copy Markdown
Contributor

jonathf commented Jan 4, 2026

This is really cool.

Do you mind sharing the snippet_helper.is_source_beginning source code? I am wondering what you do there.

@ColinKennedy
Copy link
Copy Markdown
Contributor Author

The code is really old and it "just worked" so I never bothered making it better or changing it. Anyway, here it is:

snippet_helper.lua
local line_begin = require("luasnip.extras.expand_conditions").line_begin

local module = {}

local in_docstring = function()
    local current_node = vim.treesitter.get_node({ buffer = 0 })

    if not current_node then
        return false
    end

    local type_name = current_node:type()

    if vim.bo.filetype == "python" then
        return type_name == "string_content"
    end

    vim.notify(
        'Type name"' .. type_name .. "\" is unknown. Cannot check if we're in a docstring.",
        vim.log.levels.ERROR
    )

    return false
end

-- Reference: https://snippets.bentasker.co.uk/page-1706031025-Trim-whitespace-from-beginning-of-string-LUA.html
local ltrim = function(text)
    return text:match "^%s*(.*)"
end

local strip_spaces = function(text)
    return text:gsub("%s+", "")
end

local and_ = function(...)
    local functions = { ... }

    return function(...)
        for _, function_ in ipairs(functions) do
            if not function_(...) then
                return false
            end
        end
        return true
    end
end

local is_line_beginning = function(line_to_cursor)
    return strip_spaces(line_to_cursor) == ""
end

local is_source_beginning = function(trigger)
    local wrapper = function(line_to_cursor)
        return line_begin(ltrim(line_to_cursor), trigger) ~= nil
    end

    return wrapper
end

local or_ = function(...)
    local functions = { ... }

    return function(...)
        for _, function_ in ipairs(functions) do
            if function_(...) then
                return true
            end
        end
        return false
    end
end

module.in_docstring = in_docstring
module.is_line_beginning = is_line_beginning
module.is_source_beginning = is_source_beginning
module.and_ = and_
module.or_ = or_

return module

@jonathf
Copy link
Copy Markdown
Contributor

jonathf commented Jan 6, 2026

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants