From da840436921f017269b4bd1cc0719a21886b104f Mon Sep 17 00:00:00 2001 From: Kyren223 Date: Wed, 24 Jun 2026 02:40:25 +0300 Subject: [PATCH] Add luasnip and use it to load language and project specific snippets, also add a bunch of mess to get a nice unwrap(), this is committed on purpose as a reference for future me, will be cleaned up in the next commit. It also touches a bunch of blink.cmp to achieve this. --- .config/nvim/lua/plugins/blink-cmp.lua | 84 +++++++++----- .config/nvim/lua/plugins/luasnip.lua | 147 +++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 .config/nvim/lua/plugins/luasnip.lua diff --git a/.config/nvim/lua/plugins/blink-cmp.lua b/.config/nvim/lua/plugins/blink-cmp.lua index 0c3f6d5..6d2a72e 100644 --- a/.config/nvim/lua/plugins/blink-cmp.lua +++ b/.config/nvim/lua/plugins/blink-cmp.lua @@ -5,10 +5,26 @@ return { version = '1.*', dependencies = { { 'Kaiser-Yang/blink-cmp-git', dependencies = { 'nvim-lua/plenary.nvim' } }, + { 'L3MON4D3/LuaSnip', version = 'v2.*' }, }, ---@module 'blink.cmp' ---@type blink.cmp.Config opts = { + snippets = { preset = 'luasnip' }, + -- snippets = { + -- expand = function(snippet) + -- require('luasnip').lsp_expand(snippet) + -- end, + -- active = function(filter) + -- if filter and filter.direction then + -- return require('luasnip').jumpable(filter.direction) + -- end + -- return require('luasnip').in_snippet() + -- end, + -- jump = function(direction) + -- require('luasnip').jump(direction) + -- end, + -- }, keymap = { preset = 'enter', [''] = {}, @@ -18,6 +34,36 @@ return { [''] = {}, [''] = { 'scroll_documentation_up', 'fallback' }, [''] = { 'scroll_documentation_down', 'fallback' }, + [''] = { + function(cmp) + local item = cmp.get_selected_item() + + if item and item.label == '.unwrap' then + vim.schedule(function() + local ctx = cmp.get_context() + if not ctx or not ctx.bounds then + return cmp.accept() + end + local partial = ctx.get_keyword() or '' + local full = cmp.get_selected_item().label + local rest = full:sub(#partial + 1 + 1) + + local row = ctx.bounds.line_number - 1 + local col = ctx.bounds.start_col + ctx.bounds.length + + local line = vim.api.nvim_buf_get_lines(ctx.bufnr, row, row + 1, false)[1] + local cursor_at_eol = col > #line + + vim.api.nvim_put({ rest }, 'c', cursor_at_eol, true) + require('luasnip').expand() + end) + return true + end + + return cmp.accept() + end, + 'fallback', + }, }, completion = { @@ -31,7 +77,16 @@ return { keyword = { range = 'full' }, -- Preselect first one, don't complete until confirmation - list = { selection = { preselect = true, auto_insert = false } }, + list = { + selection = { + preselect = true, + -- auto_insert = false, + auto_insert = function(ctx) + -- vim.notify() + return ctx.get_keyword() == 'unwrap' + end, + }, + }, menu = { draw = { @@ -82,31 +137,7 @@ return { }, snippets = { - -- score_offset = 200, -- make snippets highest priority - transform_items = function(_, items) - return vim.tbl_filter(function(item) - -- vim.print(item) - if item.kind ~= require('blink.cmp.types').CompletionItemKind.Snippet then - return true - end - - local name = item.description - -- vim.print(name) - local parts = vim.split(name, ' ', { trimempty = false }) - local namespace = #parts > 1 and parts[1] or nil - -- vim.print(namespace) - if not namespace then - return true - end - - -- vim.print(vim.fn.getcwd()) - local path = vim.split(vim.fn.getcwd(), '/') - local dir = path[#path] - -- vim.print(dir) - - return dir == namespace - end, items) - end, + score_offset = 200, -- make snippets highest priority }, lsp = { @@ -181,7 +212,6 @@ return { opts_extend = { 'sources.default' }, -- TODO: pressing backk (deleting), should re-show completion menu - -- TODO: ghost text only for LLMs? } -- From LazyVim diff --git a/.config/nvim/lua/plugins/luasnip.lua b/.config/nvim/lua/plugins/luasnip.lua new file mode 100644 index 0000000..39debb3 --- /dev/null +++ b/.config/nvim/lua/plugins/luasnip.lua @@ -0,0 +1,147 @@ +return { + 'L3MON4D3/LuaSnip', + version = 'v2.*', -- follow latest release. + build = 'make install_jsregexp', + opts = {}, + config = function(_, opts) + require('luasnip').setup(opts) + + -- Load language specific snippets + require('luasnip.loaders.from_vscode').load({ paths = './snippets' }) + + -- load project specific snippets + local project = vim.fn.fnamemodify(vim.fn.getcwd(), ':t') + local project_snippets = { + eko = 'eko.json', + ['carbonight.nvim'] = 'carbonight.json', + } + local file = project_snippets[project] + if file then + require('luasnip.loaders.from_vscode').load_standalone({ path = './snippets/' .. file }) + end + + -- some shorthands... + local ls = require('luasnip') + local s = ls.snippet + local sn = ls.snippet_node + local isn = ls.indent_snippet_node + local t = ls.text_node + local i = ls.insert_node + local f = ls.function_node + local c = ls.choice_node + local d = ls.dynamic_node + local r = ls.restore_node + local events = require('luasnip.util.events') + local ai = require('luasnip.nodes.absolute_indexer') + local opt = require('luasnip.nodes.optional_arg') + local extras = require('luasnip.extras') + local l = extras.lambda + local rep = extras.rep + local p = extras.partial + local m = extras.match + local n = extras.nonempty + local dl = extras.dynamic_lambda + local fmt = require('luasnip.extras.fmt').fmt + local fmta = require('luasnip.extras.fmt').fmta + local conds = require('luasnip.extras.expand_conditions') + local postfix = require('luasnip.extras.postfix').postfix + local ts_postfix = require('luasnip.extras.treesitter_postfix').treesitter_postfix + local types = require('luasnip.util.types') + local parse = require('luasnip.util.parser').parse_snippet + local ms = ls.multi_snippet + local k = require('luasnip.nodes.key_indexer').new_key + local postfix_builtin = require('luasnip.extras.treesitter_postfix').builtin + + ls.add_snippets('all', { + ts_postfix({ + matchTSNode = postfix_builtin.tsnode_matcher.find_topmost_types({ + 'identifier', + 'method_invocation', + 'field_access', + 'type_identifier', + 'class_literal', + 'this', + 'scoped_type_identifier', + }), + trig = '.unwrap', + }, { + f(function(_, parent) + -- Use POSTFIX_MATCH instead of LS_TSMATCH + local match = parent.snippet.env.LS_TSMATCH + -- vim.notify('postfix: "' .. vim.inspect(match) .. '"') + + if not match then + return { 'unwrap()' } + end + + -- If LuaSnip provides the match as a table, concat it + if type(match) == 'table' then + match = table.concat(match, '\n') + end + + local wrapped = string.format('unwrap(%s)', match) + -- Split it back into a table of strings to safely handle any newlines + return vim.split(wrapped, '\n', { plain = true }) + end), + }), + + -- ts_postfix({ + -- trig = 'unwrap', + -- matchTSNode = { + -- query = [[ + -- (field_access + -- object: ( + -- field_access + -- method_invocation + -- identifier + -- ) @my_object + -- ) @my_field + -- ]], + -- query_lang = 'java', + -- select = 'longest', + -- }, + -- }, { + -- f(function(_, parent) + -- -- Use POSTFIX_MATCH instead of LS_TSMATCH + -- local match = parent.snippet.env.POSTFIX_MATCH + -- vim.notify('postfix: "' .. vim.inspect(match) .. '"') + -- + -- if not match then + -- return { 'unwrap()' } + -- end + -- + -- -- If LuaSnip provides the match as a table, concat it + -- if type(match) == 'table' then + -- match = table.concat(match, '\n') + -- end + -- + -- -- Format the string + -- local wrapped = string.format('unwrap(%s)', match) + -- + -- -- Split it back into a table of strings to safely handle any newlines + -- return vim.split(wrapped, '\n', { plain = true }) + -- end), + -- }, { condition = require('luasnip.util.util').yes }), + }) + -- postfix({ trig = 'unwrap', match_pattern = [[[%w%.%_%-%"%']+$]] }, { + -- f(function(_, parent) + -- local match = parent.snippet.env.POSTFIX_MATCH + -- if match == nil then + -- return 'bunger' + -- end + -- -- match = match:sub(1, -2) + -- return 'unwrap(' .. match .. ')' + -- end, {}), + -- }, { condition = require('luasnip.util.util').yes }), + + -- postfix({ trig = '.unwrap', snippetType = 'snippet' }, { + -- f(function(_, parent) + -- local match = parent.snippet.env.POSTFIX_MATCH + -- if match == nil then + -- return 'bunger' + -- end + -- return '[' .. match .. ']' + -- end, {}), + -- }), + end, +}