LuaSnip icon indicating copy to clipboard operation
LuaSnip copied to clipboard

[FeatureRequest] Ability to extend snippet definitions NOT on filetype

Open DeadlySquad13 opened this issue 3 years ago • 11 comments

Hello, thanks for your plugin! I was migrating from ultisnips as I had performance issues on my wsl machine - with luasnip everything works lightning fast! However, I haven't found one particular mechanism that I was using earlier: ability to load specific snippets without relying on particular filetype.

Problem description

For example, I have a plugins.lua with packer startup function. Here I want to access autoexpanded snippets like: use -> use({ 'L3MON4D3/LuaSnip', config = ... }) Unfortunately, I have found only one way - define it in snippets for lua filetype. But it's too obtrusive in other lua files, I need it only in my plugins.lua. I suspect that I can manually load and unload snippets by using autocommands and provided api but it seems as a dirty solution to me. To understand what I mean, look at my use case.

Solution in UltiSnips

local utils = require('utils');
local create_augroup = utils.create_augroup;
local create_autocmd = utils.create_autocmd;

-- NOTE: clear - clears previously created autocommands in augroup.

local snippets = create_augroup('CustomSnippets', { clear = true });

local BufEarlyEnter = { 'BufNewFile', 'BufReadPost' }
-- Encapsulating some options that are commonly used by snippet autocmds. 
local snippets_create_autocmd = function(options)
  -- Will raise an error if you try to overwrite some value in the left table.
  create_autocmd(BufEarlyEnter, vim.tbl_deep_extend('error', { group = snippets }, options));
end

snippets_create_autocmd({
  desc = 'Added packer.snippets as a main snippet definition source while editing plugins file',

  pattern = 'plugins.lua',
  command = 'UltiSnipsAddFiletypes packer.lua',
});

As you can see, in the autocommand I have a special command that builds up a hierarchy: UltiSnipsAddFiletypes packer.lua means that packer snippets take priority over lua snippets. Later on, while editing plugins.lua packer is interpreted like one of the sources for snippet definitions and on issuing the command UltiSnipEdit the packer snippets are opened automagically. In LuaSnip I expect to see something like this: image

Possible solutions

  1. Change filetype on plugins.lua to packer. For me it seems like a hack that may also break something in other plugins along the road.
  2. I have found methods that extend / overwrite snippets definitions:
-- in a lua file: search lua-, then c-, then all-snippets.
ls.filetype_extend("lua", { "c" })
-- in a cpp file: search c-snippets, then all-snippets only (no cpp-snippets!!).
ls.filetype_set("cpp", { "c" })

I think that it's a good way to build hierarchy mentioned earlier but it's only applied to filetypes. I would love to see functions that make it possible to create connections between definitions by custom autocommands / functions rather than only filetypes. In my mind it would look like this:

snippets_create_autocmd({
  desc = 'Added packer.snippets as a main snippet definition source while editing plugins file',

  pattern = 'plugins.lua',
  callback = function()
     ls.definitions_extend('lua', { 'packer' });
  end
});

I think this feature deserves attention as it helps with dealing with specific files making your snippet management easier! Hope you will help me solve this issue!

DeadlySquad13 avatar Jun 27 '22 11:06 DeadlySquad13

Hey, glad to hear that :D

Thank you for the detailed issue, your second solution is actually already possible: Luasnip supports a callback for determining the filetype(s) effective at any position (ft_func in setup, the default is to just check &filetype), it could check the buffername and add filetypes:

local default_filetype_load_function = require("luasnip.extras.filetype_functions").from_filetype_load

-- map filename (:t) to custom filetypes.
file_extends = {
	"packer.lua" = {"packer"}
}

function resolve_filetypes(bufnr)
	return vim.list_extend(
		-- file_extends has higher priority than default.
		file_extends[vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ":t")],
		default_filetype_load_function(bufnr) )
end

ls.setup({
	-- for regular expansion and nvim-cmp.
	ft_func = function()
		return resolve_filetypes(vim.api.nvim_get_current_buf())
	end,
	-- also override load_ft_func so `packer`-snippets are lazy_loaded.
	load_ft_func = resolve_filetypes
})

(I didn't test that, but it should at least almost work :D)

L3MON4D3 avatar Jun 27 '22 18:06 L3MON4D3

Hey, glad to hear that :D

Thank you for the detailed issue, your second solution is actually already possible: Luasnip supports a callback for determining the filetype(s) effective at any position (ft_func in setup, the default is to just check &filetype), it could check the buffername and add filetypes:

local default_filetype_load_function = require("luasnip.extras.filetype_functions").from_filetype_load

-- map filename (:t) to custom filetypes.
file_extends = {
	"packer.lua" = {"packer"}
}

function resolve_filetypes(bufnr)
	return vim.list_extend(
		-- file_extends has higher priority than default.
		file_extends[vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ":t")],
		default_filetype_load_function(bufnr) )
end

ls.setup({
	-- for regular expansion and nvim-cmp.
	ft_func = function()
		return resolve_filetypes(vim.api.nvim_get_current_buf())
	end,
	-- also override load_ft_func so `packer`-snippets are lazy_loaded.
	load_ft_func = resolve_filetypes
})

(I didn't test that, but it should at least almost work :D)

Oh, thanks for the code, I will try it out tomorrow! But doesn't it have essentially the same issue as my first solution: does it polute the global scope of filetypes by this custom filetype?

DeadlySquad13 avatar Jun 27 '22 18:06 DeadlySquad13

Oh, no worries, this won't affect any global state, ft_func is only used for determining the filetypes that should be searched for snippets.

L3MON4D3 avatar Jun 27 '22 19:06 L3MON4D3

Oh, no worries, this won't affect any global state, ft_func is only used for determining the filetypes that should be searched for snippets.

Oh, nice to hear. I tried your solution, had to change a few things and now it works, big thanks! However, it seems that something is causing a loop as I get this much results in snippet definitions list: image

I printed out results of a extended_filetype_load_function, and got following results:

  • in *.lua: image
  • in packer.lua: image

My code:

local luasnip = require("luasnip");
local default_filetype_load_function = require("luasnip.extras.filetype_functions").from_filetype_load;

-- map filename (:t) to custom filetypes.
local file_extends = {
	['plugins.lua'] = { 'packer' },
}

local function extended_filetype_load_function(bufnr)
  return file_extends[
    vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ":t")
  ] or {}; -- if we haven't found anything, return {}.
end

local function resolve_filetypes(bufnr)
  return vim.list_extend(
     -- file_extends has higher priority than default.
     extended_filetype_load_function(bufnr),
     default_filetype_load_function(bufnr)
  );
end

---@see <https://github.com/L3MON4D3/LuaSnip#config> 'Config section in LuaSnip
--docs'.
luasnip.config.set_config({
  -- If true, Snippets that were exited can still be jumped back into.
  history = true,

	-- for regular expansion and nvim-cmp.
  ft_func = function()
    return resolve_filetypes(vim.api.nvim_get_current_buf())
  end,
	-- also override load_ft_func so `packer`-snippets are lazy_loaded.
  load_ft_func = resolve_filetypes
});

Can you please help, what's causing this loop and how can I fix it?

DeadlySquad13 avatar Jun 28 '22 12:06 DeadlySquad13

Ahh of course, vim.list_extend modifies file_extends xD I think vim.list_extend also works with more than two lists, so extend({}, file_extends, default) should do ot

L3MON4D3 avatar Jun 28 '22 13:06 L3MON4D3

Is it no simpler just to have a custom filetype (let's say packer_ft) and set the filetype of that file to lua.packer_ft and then add the custom snippets to the packer_ft filetype?

leiserfg avatar Jun 28 '22 13:06 leiserfg

True, that would be much simpler, but I get not wanting to modify the filetype just for snippets if it's possible in a way that is only seen by luasnip.

Just saw that there are issues besides the duplicated filetype, not quite sure where the empty strings are coming from..

L3MON4D3 avatar Jun 28 '22 14:06 L3MON4D3

Ahh of course, vim.list_extend modifies file_extends xD I think vim.list_extend also works with more than two lists, so extend({}, file_extends, default) should do ot

Unfortunately, no, It works only with two lists.

list_extend({dst}, {src}, {start}, {finish})               *vim.list_extend()*
                Extends a list-like table with the values of another list-like
                table.

                NOTE: This mutates dst!

                Parameters: ~
                    {dst}     (table) List which will be modified and appended
                              to
                    {src}     (table) List from which values will be inserted
                    {start}   (number) Start index on src. Defaults to 1
                    {finish}  (number) Final index on src. Defaults to `#src`

                Return: ~
                    (table) dst

                See also: ~
                    |vim.tbl_extend()|

DeadlySquad13 avatar Jun 28 '22 16:06 DeadlySquad13

Ahh of course, vim.list_extend modifies file_extends xD I think vim.list_extend also works with more than two lists, so extend({}, file_extends, default) should do ot

I think I figured it out, thanks for the hint! image Code:

local luasnip = require("luasnip");
local default_filetype_load_function = require("luasnip.extras.filetype_functions").from_filetype_load;

-- map filename (:t) to custom filetypes.
local file_extends = {
	['plugins.lua'] = { 'packer' },
}

local function extended_filetype_load_function(bufnr)
  return file_extends[
    vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ":t")
  ] or {}; -- if we haven't found anything, return {}.
end

-- Doesn't modify the initial_list and accepts variable number of parameters.
local function list_deep_extend(initial_list, ...)
  local args = {...}
  local result = vim.deepcopy(initial_list);

  for _, values in ipairs(args) do
    vim.list_extend(result, values);
  end

  return result;
end

local function resolve_filetypes(bufnr)
  return list_deep_extend(
    {},
		-- file_extends has higher priority than default.
    extended_filetype_load_function(bufnr),
    default_filetype_load_function(bufnr)
  );
end

---@see <https://github.com/L3MON4D3/LuaSnip#config> 'Config section in LuaSnip
--docs'.
luasnip.config.set_config({
  -- If true, Snippets that were exited can still be jumped back into.
  history = true,

	-- for regular expansion and nvim-cmp.
  ft_func = function()
    return resolve_filetypes(vim.api.nvim_get_current_buf())
  end,
	-- also override load_ft_func so `packer`-snippets are lazy_loaded.
  load_ft_func = resolve_filetypes
});

The only thing that still bothers me: how to extend this algorithm to match glob patterns like '*.sync.py' = 'jupyter_ascending' (I used this pattern to load my snippets for jupyter_ascending only in files ending on .sync.py). I would be grateful If you show me direction to search.

Thank you for participation, you showed me the crucial parts of the api needed to implement this feature. However, as for me, it's a bit too low level. Most people won't be able to dig this out so I still think that it's better to implement it in plugin they same way as filetype_extend is implemented. Nevertheless, if it's too far from your roadmap, feel free to close this issue as personally I have solved my problem. Hope your plugin grows and gets attention it deserves!

DeadlySquad13 avatar Jun 28 '22 17:06 DeadlySquad13

The only thing that still bothers me: how to extend this algorithm to match glob patterns like '*.sync.py' = 'jupyter_ascending' (I used this pattern to load my snippets for jupyter_ascending only in files ending on .sync.py). I would be grateful If you show me direction to search.

You could store lua-patterns in file_extends, iterate over those and get the filetypes from the one (if any) that matches (basically filename:match(file_extends_pattern))

However, as for me, it's a bit too low level. Most people won't be able to dig this out so I still think that it's better to implement it in plugin they same way as filetype_extend is implemented.

Mhmm, maybe not the same way as file_extends, but we could add it to extras.filetype_functions, all one would have to do as a user is provide the table mapping pattern -> filetypes, and that would be it.

It might be low level, but these (interfaces? configuration-points?) are much more flexible than lots of single function that do some specific thing, I think we should keep it mostly this way.

L3MON4D3 avatar Jun 28 '22 19:06 L3MON4D3

It might be low level, but these (interfaces? configuration-points?) are much more flexible than lots of single function that do some specific thing, I think we should keep it mostly this way.

Yes, I feel the same, it's always nice to have low level api for advanced usage / integrations. After all, I think for most people it's a primary reason to use neovim over other editors.

DeadlySquad13 avatar Jun 28 '22 20:06 DeadlySquad13