-- nixos-config - My current NixOS configuration -- -- Copyright (C) 2025 Benedikt Peetz -- SPDX-License-Identifier: GPL-3.0-or-later -- -- This file is part of my nixos-config. -- -- You should have received a copy of the License along with this program. -- If not, see . local ls = require("luasnip") local fmt = require("luasnip.extras.fmt").fmt --- Get the comment string {begin,end} table --- ---@param comment_type integer 1 for `line`-comment and 2 for `block`-comment ---@return table comment_strings {["begin"]=begin_comment_string, ["end"]=end_comment_string} local get_comment_string = function(comment_type) local calculate_comment_string = require("Comment.ft").calculate local utils = require("Comment.utils") -- use the `Comments.nvim` API to fetch the comment string for the region (eq. '--%s' or '--[[%s]]' for `lua`) local cstring = calculate_comment_string({ ctype = comment_type; range = utils.get_region(); }) if cstring == nil then -- TODO: Use `vim.bo.commentstring` <2025-05-02> -- Use some useful default values. return { ["begin"] = "#"; ["end"] = ""; } end -- as we want only the strings themselves and not strings ready for using `format` we want to split the left and right side local left, right = utils.unwrap_cstr(cstring) -- create a `{left, right}` table for it return { ["begin"] = left; ["end"] = right; } end --- Wraps a table of snippet nodes in two comment function nodes. --- ---@param comment_type integer 1 for `line`-comment and 2 for `block`-comment ---@param nodes table The nodes that should be wrapped ---@return table wrapped_nodes The now wrapped `nodes` table. local wrap_snippet_in_comments = function(comment_type, nodes) local output = {} table.insert(output, ls.function_node(function() return get_comment_string(comment_type)["begin"] end)) for _, v in ipairs(nodes) do table.insert(output, v) end table.insert(output, ls.function_node(function() return get_comment_string(comment_type)["end"] end)) return output end -- auto_pairs {{{ local get_visual = function(_, parent) if #parent.snippet.env.SELECT_RAW > 0 then return ls.snippet_node(nil, ls.insert_node(1, parent.snippet.env.SELECT_RAW)) else return ls.snippet_node(nil, ls.insert_node(1, "")) end end local function char_count_same(c1, c2) local line = vim.api.nvim_get_current_line() -- '%'-escape chars to force explicit match (gsub accepts patterns). -- second return value is number of substitutions. local _, ct1 = string.gsub(line, "%" .. c1, "") local _, ct2 = string.gsub(line, "%" .. c2, "") return ct1 == ct2 end local function even_count(c, ...) local line = vim.api.nvim_get_current_line() local _, ct = string.gsub(line, c, "") return ct % 2 == 0 end -- This makes creation of pair-type snippets easier. local function pair(pair_begin, pair_end, file_types, condition_function) -- FIXME(@Soispha): This only works if file_types == nil, otherwise the snippet does not expand. -- It would be nice, if it would support both an empty array (`{}`) and nil <2023-08-27> -- file_types = file_types or {}; return ls.snippet( { trig = pair_begin; wordTrig = false; snippetType = "autosnippet"; }, { ls.text_node({ pair_begin; }); ls.dynamic_node(1, get_visual); ls.text_node({ pair_end; }); }, { condition = function() local filetype_check = true if file_types ~= nil then filetype_check = file_types[vim.bo.filetype] or false end return (not condition_function(pair_begin, pair_end)) and filetype_check end; } ) end local auto_pairs = { pair("(", ")", nil, char_count_same); pair("{", "}", nil, char_count_same); pair("[", "]", nil, char_count_same); pair("<", ">", { ["rust"] = true; ["tex"] = true; }, char_count_same); pair("'", "'", nil, even_count); pair("\"", "\"", nil, even_count); pair("`", "`", nil, even_count); } ls.add_snippets("all", auto_pairs, { type = "snippets"; key = "auto_pairs"; }) -- }}} -- todo_comments {{{ local read_git_config = function(config_value) local command = string.format("git config \"%s\"", config_value) local handle = io.popen(command) if handle == nil then return error(string.format("Failed to call `%s`.", command)) end local result = handle:read("*a") handle:close() -- stripped = string.gsub(str, '%s+', '') return string.gsub(result, "\n", "") end --- Create a @handle from a full name. --- --- Example: --- “Benedikt Peetz” -> “@bpeetz” local handle_from_name = function(name) -- from: https://stackoverflow.com/a/7615129 local split = function(inputstr, sep) local t = {} for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do table.insert(t, str) end return t end -- Split on spaces local parts = split(name, "%s") local output_name = "" if #parts > 2 then -- Only use the first chars. -- -- Example: -- “Richard Matthew Stallman” -> “rms” for _, val in ipairs(parts) do output_name = string.format("%s%s", output_name, val:sub(1, 1)) end elseif #parts == 2 then output_name = string.format("%s%s", parts[1]:sub(1, 1), parts[2]) elseif #parts == 1 then output_name = parts[1] elseif #parts == 0 then output_name = "" end return string.format("@%s", output_name:lower()) end --- Generate a comment snippet --- ---@param trig string The trigger ---@param name string name for the comment (ex.: {FIX, ISSUE, FIXIT, BUG}) ---@param comment_type integer The comment type. ---@param mark_function function: The function used to get the marks local todo_snippet = function(trig, name, comment_type, mark_function) assert(trig, "context doesn't include a `trig` key which is mandatory") assert(comment_type == 1 or comment_type == 2) local context = {} context.name = name .. " comment" context.trig = trig local date_node, signature_node = mark_function() local nodes = fmt("{} {}{}: {} {} {}", wrap_snippet_in_comments(comment_type, { ls.text_node(name); signature_node; ls.insert_node(1, "content"); date_node; })) return ls.snippet(context, nodes, { ctype = comment_type; }) end ---@param trigger string: The luasnip trigger ---@param comment_type integer: The luasnip comment type ---@param name string: All aliases for a name ---@return table: All possible snippets build from the marks local process_marks = function(trigger, name, comment_type) local username = function() return handle_from_name(read_git_config("user.name")) end local marks = { signature = function() return ls.text_node("(" .. username() .. ")"), ls.text_node("") end; date_signature = function() return ls.text_node("<" .. os.date("%Y-%m-%d") .. ">"), ls.text_node("(" .. username() .. ")") end; date = function() return ls.text_node("<" .. os.date("%Y-%m-%d") .. ">"), ls.text_node("") end; empty = function() return ls.text_node(""), ls.text_node("") end; } local output = {} for mark_name, mark_function in pairs(marks) do local trig = trigger .. "-" .. mark_name output[#output + 1] = todo_snippet(trig, name, comment_type, mark_function) end return output end local todo_snippet_specs = { { { trig = "todo"; }; { "TODO"; }; { ctype = 1; }; }; { { trig = "fix"; }; { "FIXME"; "ISSUE"; }; { ctype = 1; }; }; { { trig = "hack"; }; { "HACK"; }; { ctype = 1; }; }; { { trig = "warn"; }; { "WARNING"; }; { ctype = 1; }; }; { { trig = "perf"; }; { "PERFORMANCE"; "OPTIMIZE"; }; { ctype = 1; }; }; { { trig = "note"; }; { "NOTE"; "INFO"; }; { ctype = 1; }; }; -- NOTE: Block commented todo-comments { { trig = "todob"; }; { "TODO"; }; { ctype = 2; }; }; { { trig = "fixb"; }; { "FIXME"; "ISSUE"; }; { ctype = 2; }; }; { { trig = "hackb"; }; { "HACK"; }; { ctype = 2; }; }; { { trig = "warnb"; }; { "WARNING"; }; { ctype = 2; }; }; { { trig = "perfb"; }; { "PERF"; "PERFORMANCE"; "OPTIM"; "OPTIMIZE"; }; { ctype = 2; }; }; { { trig = "noteb"; }; { "NOTE"; "INFO"; }; { ctype = 2; }; }; } local todo_comment_snippets = {} for _, v in ipairs(todo_snippet_specs) do local snippets = process_marks(v[1].trig, v[2][1], v[3].ctype) for _, value in pairs(snippets) do table.insert(todo_comment_snippets, value) end end ls.add_snippets("all", todo_comment_snippets, { type = "snippets"; key = "todo_comments"; }) -- }}} -- spdx snippets {{{ local generate_spdx_snippet = function(comment_type, spdx_license_expr, trigger) assert(trigger, "context doesn't include a `trig` key which is mandatory") assert(comment_type == 1 or comment_type == 2) local context = {} context.name = trigger .. " spdx snippet expr" context.trig = trigger local nodes = { fmt("{} SPDX-SnippetBegin {}", wrap_snippet_in_comments(comment_type, {})); fmt("{} SPDX-SnippetCopyrightText: {} {} <{}> {}", wrap_snippet_in_comments(comment_type, { ls.insert_node(1, "year"); ls.insert_node(2, "author"); ls.insert_node(3, "email"); }) ); fmt("{} SPDX-License-Identifier: {} {}", wrap_snippet_in_comments(comment_type, { ls.text_node(spdx_license_expr); })); { ls.insert_node(4, "content"); }; fmt("{} SPDX-SnippetEnd {}", wrap_snippet_in_comments(comment_type, {})); { ls.insert_node(0); }; } local newline_nodes = {} for _, sub_nodes in ipairs(nodes) do for _, node in ipairs(sub_nodes) do table.insert(newline_nodes, node) end -- luasnip requires newlines to be encoded like this: table.insert(newline_nodes, ls.text_node({ ""; ""; })) end return ls.snippet(context, newline_nodes, { ctype = comment_type; }) end local spdx = { { trigger = "spdx-AGPL3+"; license = "AGPL-3.0-or-later"; }; { trigger = "spdx-GPL3+"; license = "GPL-3.0-or-later"; }; { trigger = "spdx-MIT"; license = "MIT"; }; } local spdx_snippets = {} for _, value in ipairs(spdx) do local snippet = generate_spdx_snippet(1, value.license, value.trigger) table.insert(spdx_snippets, snippet) snippet = generate_spdx_snippet(2, value.license, value.trigger .. "-block") table.insert(spdx_snippets, snippet) end ls.add_snippets("all", spdx_snippets, { type = "snippets"; key = "spdx_snippets"; }) -- }}}