Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beta-testing 'mini.snippets' #1428

Open
echasnovski opened this issue Dec 23, 2024 · 68 comments
Open

Beta-testing 'mini.snippets' #1428

echasnovski opened this issue Dec 23, 2024 · 68 comments

Comments

@echasnovski
Copy link
Owner

Please leave your feedback about new mini.snippets module here. Feel free to either add new comment or positively upvote existing one.

Some things I am interested to find out (obviously, besides bugs):

  • Does it work well out of the box with recommended "Quickstart" setup?
  • Is configuration intuitive enough?
  • Are default values of settings convenient for you?
  • Is documentation and examples clear enough?

Thanks!

@231tr0n
Copy link

231tr0n commented Dec 23, 2024

Yay!! Christmas present from the mini.nvim santa has dropped. As usual awesome work @echasnovski. I had few doubts

  1. Are snippets populated in mini.completion? Because, I just tried it in a go file and snippets are not showing up.
  2. Is running the below snippet enough to load snippets from the above plugin. Because, when I try to expand it using C-j, it prints out this error message.
    image
	add("rafamadriz/friendly-snippets")
	require("mini.snippets").setup()

@echasnovski
Copy link
Owner Author

  1. Are snippets populated in mini.completion? Because, I just tried it in a go file and snippets are not showing up.

No, not yet. I hope this to get resolved next (#886).

2. Is running the below snippet enough to load snippets from the above plugin

No, 'mini.snippets' by default doesn't load any snippets. This has to be done explicitly by the user (similar to 'mini.hipatterns'). The setup from Quickstart should work.

@231tr0n

This comment was marked as resolved.

@echasnovski

This comment was marked as outdated.

@231tr0n

This comment was marked as resolved.

@echasnovski

This comment was marked as resolved.

@231tr0n
Copy link

231tr0n commented Dec 23, 2024

One last ask, is it possible to provide an example on how to configure mini.snippets with rafamadriz/friendly-snippets in the docs.

There is. Quickstart.

So, basically gen_loader.from_lang() does the job of loading the snippets right?

@echasnovski
Copy link
Owner Author

One last ask, is it possible to provide an example on how to configure mini.snippets with rafamadriz/friendly-snippets in the docs.

There is. Quickstart.

So, basically gen_loader.from_lang() does the job of loading the snippets right?

Correct. The way it works is designed after 'rafamadriz/friendly-snippets'. Plus it is a reasonable way to organize per language snippets anyway.

@231tr0n
Copy link

231tr0n commented Dec 23, 2024

Ok! I got it working but, C-h and C-l do not work or navigate for lsp completed snippets
image

@echasnovski
Copy link
Owner Author

echasnovski commented Dec 23, 2024

Ok! I got it working but, C-h and C-l do not work or navigate for lsp completed snippets

Yes, they do not. Because #886 is still not resolved.

Edit: 'mini.snippets' is expected to be used with manual expansion (<C-j> by default): then it starts snippet session and <C-l> and <C-h> work. This makes it more self contained and not depend on (auto)completion framework, which I personally prefer.

During work on #886 I plan to provide a way to define 'mini.snippets' as default snippet expand/insert for snippets from LSP completions and have a way for 'mini.snippets' to provide its suggestions for 'mini.completion' (as a dummy LSP server). These are distinct features which come from the fact that 'mini.snippets' is both snippet manager (find/match snippet) and expand provider (start snippet session, jump between tabstops, etc.).

@231tr0n
Copy link

231tr0n commented Dec 23, 2024

Ok! I got it working but, C-h and C-l do not work or navigate for lsp completed snippets image

Yes, they do not. Because #886 is still not resolved.

Ohh ok my bad, did not think these two were dependent for working. Overall, the module looks great. I just tried writing basic boiler plate code and I write it bonkers fast now. Thank you once again @echasnovski .

@xzbdmw
Copy link
Contributor

xzbdmw commented Dec 23, 2024

I find "c-n" does not work given the config, it delete all the chars before cursor pos, after I expand and then use "right" to move one char, "c-n" works.

local root = vim.fn.fnamemodify("./.repro", ":p")
-- set stdpaths to use .repro
for _, name in ipairs({ "config", "data", "state", "cache" }) do
    vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end
-- bootstrap lazy
local lazypath = root .. "/plugins/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
    vim.fn.system({
        "git",
        "clone",
        "--filter=blob:none",
        "--single-branch",
        "https://github.com/folke/lazy.nvim.git",
        lazypath,
    })
end
vim.opt.runtimepath:prepend(lazypath)
-- install plugins
local plugins = {
    "folke/tokyonight.nvim",
    -- do not remove the colorscheme!
    {
        "echasnovski/mini.snippets",
        version = false,
        config = function()
            local gen_loader = require("mini.snippets").gen_loader
            require("mini.snippets").setup({
                snippets = {
                    -- Load custom file with global snippets first (adjust for Windows)
                    gen_loader.from_file("~/.config/nvim/snippets/global.json"),

                    -- Load snippets based on current language by reading files from
                    -- "snippets/" subdirectories from 'runtimepath' directories.
                    gen_loader.from_lang(),
                },
                -- Module mappings. Use `''` (empty string) to disable one.
                mappings = {
                    -- Expand snippet at cursor position. Created globally in Insert mode.
                    expand = "<C-7>",

                    -- Interact with default `expand.insert` session.
                    -- Created for the duration of active session(s)
                    jump_next = "<C-n>",
                    jump_prev = "<C-p>",
                    stop = "<C-c>",
                },

                -- Functions describing snippet expansion. If `nil`, default values
                -- are `MiniSnippets.default_<field>()`.
                expand = {
                    -- Resolve raw config snippets at context
                    prepare = nil,
                    -- Match resolved snippets at cursor position
                    match = nil,
                    -- Possibly choose among matched snippets
                    select = nil,
                    -- Insert selected snippet
                    insert = nil,
                },
            })
        end,
    },

    -- add any other pugins here
}

require("lazy").setup(plugins, {
    root = root .. "/plugins",
})
vim.cmd([[colorscheme tokyonight]])

global.json

{
	"Basic": {
		"prefix": "ba",
		"body": "T1=$1 T2=$2 T0=$0"
	},
}
iShot_2024-12-24_00.00.29.mp4

@echasnovski
Copy link
Owner Author

echasnovski commented Dec 23, 2024

I find "c-n" does not work given the config, it delete all the chars before cursor pos, after I expand and then use "right" to move one char, "c-n" works.

Yeah, unfortunately, <C-n> and <C-p> are somewhat special because they are forced by built-in Insert completion. My suggestion would be to not use those keys, as 'mini.snippets' has to deal with built-in completion for tabstops with choices. I've spent too much time overcoming its quirks when it comes to handling some common cases when used with 'mini.completion' (there is a separate test set).

I'll take a closer look to try and tackle this case, but I have a feeling that this will just be a documented known limitation. Thanks for the feedback!

@xzbdmw
Copy link
Contributor

xzbdmw commented Dec 23, 2024

I tried to remap <c-n> to force using MiniSnippets.session.jump("next") but with no luck...

@echasnovski
Copy link
Owner Author

I tried to remap <c-n> to force using MiniSnippets.session.jump("next") but with no luck...

That's already what those mapping do (but only for the duration of snippet session).

The issue here is that 'mini.snippets' sues vim.fn.complete(..., {}) to force hide completion popup. For certain complicated reasons, it has to be done on every jump regardless on whether popup is shown (or at least it is the best solution I could find). The vim.fn.complete() call with empty array of items still acts as if completion is active (which is questionable, I think) and with it both <C-n> and <C-p> are forcefully act to go down and up completion list. Here is a more or less related issue: vim/vim#6440

You can see how that looks with the following:

  • nvim --clean.
  • :inoremap <C-n> w
  • Type text aa bb and on new line press <C-x><C-n>. This starts built-in completion. Now pressing <C-n> moves down the completion list. If there is no completion, pressing <C-n> just inserts w.

At the moment, I think that making vim.fn.complete(..., {}) actually stop current completion and not start new one is the best way forward. But that requires writing Vim C patch... I'll take a look.

@xzbdmw
Copy link
Contributor

xzbdmw commented Dec 23, 2024

Oh, thanks for explanation -- I see many hard coded motions, there are even hard coded keymaps!

@xzbdmw
Copy link
Contributor

xzbdmw commented Dec 23, 2024

Stay in insert mode is superior, no longer need to escape select mode when jump to previous tabstop! I'm looking to write a cmp-minisnippets.

@xzbdmw
Copy link
Contributor

xzbdmw commented Dec 24, 2024

I notice one very strange thing when using cmp, some chars are swallowed when expanding:
typing sequence : vim.sc<cr>fun<cr> result in right pair missing;
typing sequence : vim.sc<cr><cr> has correct result,
typing sequence : vim.schedule()<left>fun<cr> has correct result,
probably a cmp bug. (neovim version 0.10.2, vim.snippet.expand and require("luasnip").lsp_expand(args.body) works fine)

iShot_2024-12-24_08.10.18.mp4
local root = vim.fn.fnamemodify("./.repro", ":p")
-- set stdpaths to use .repro
for _, name in ipairs({ "config", "data", "state", "cache" }) do
    vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end
-- bootstrap lazy
local lazypath = root .. "/plugins/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
    vim.fn.system({
        "git",
        "clone",
        "--filter=blob:none",
        "--single-branch",
        "https://github.com/folke/lazy.nvim.git",
        lazypath,
    })
end
vim.opt.runtimepath:prepend(lazypath)
-- install plugins
local plugins = {
    -- do not remove the colorscheme!
    "folke/tokyonight.nvim",
    "neovim/nvim-lspconfig",
    {
        "hrsh7th/nvim-cmp",
        lazy = false,
        dependencies = {
            "hrsh7th/cmp-nvim-lsp",
        },
        config = function(_, opts)
            local cmp = require("cmp")
            require("cmp").setup({
                mapping = cmp.mapping.preset.insert({
                    ["<cr>"] = cmp.mapping(function(fallback)
                        if cmp.visible() then
                            cmp.confirm()
                        end
                    end, { "i", "c", "s" }),
                }),
                completion = {
                    completeopt = "menu,menuone,noinsert",
                },
                snippet = {
                    expand = function(args)
                        local insert = MiniSnippets.config.expand.insert or MiniSnippets.default_insert
                        insert({ body = args.body })
                    end,
                },
                sources = require("cmp").config.sources({
                    { name = "nvim_lsp" },
                }, {}),
            })
        end,
    },
    {
        "echasnovski/mini.snippets",
        version = false,
        config = function()
            local gen_loader = require("mini.snippets").gen_loader
            require("mini.snippets").setup({
                snippets = {
                    -- Load custom file with global snippets first (adjust for Windows)
                    gen_loader.from_file("~/.config/nvim/snippets/global.json"),

                    -- Load snippets based on current language by reading files from
                    -- "snippets/" subdirectories from 'runtimepath' directories.
                    gen_loader.from_lang(),
                },
            })
        end,
    },
    -- add any other pugins here
}

require("lazy").setup(plugins, {
    root = root .. "/plugins",
})

require("lspconfig").lua_ls.setup({
    settings = {
        capabilities = require("cmp_nvim_lsp").default_capabilities(),
        Lua = {
            runtime = {
                version = "LuaJIT",
            },
            workspace = {
                library = {
                    "/usr/local/share/nvim/runtime",
                },
            },
            completion = {
                callSnippet = "Replace",
            },
        },
    },
})
vim.cmd([[colorscheme tokyonight]])

@echasnovski
Copy link
Owner Author

I notice one very strange thing when using cmp, some chars are swallowed when expanding:
typing sequence : vim.sc<cr>fun<cr> result in right pair missing;
typing sequence : vim.sc<cr><cr> has correct result,
typing sequence : vim.schedule()<left>fun<cr> has correct result,
probably a cmp bug. (neovim version 0.10.2, vim.snippet.expand and require("luasnip").lsp_expand(args.body) works fine)

Hmmm... I have doubts that this is from 'mini.snippets' as it during initial insert it doesn't remove any text. But the fact that vim.snippet.expand() and 'LuaSnip' work make me doubt my doubts.

@xzbdmw
Copy link
Contributor

xzbdmw commented Dec 24, 2024

I debug to find cmp removing the pair before snippet expanding, needs more investment.

The whole process is following, | stands for cursor:

  1. type vim.sc<cr> expanding to vim.schedule(|fn)
  2. lua_ls interprets it as if user types vim.schedule(fn) suggesting fun as a snippet completion item
  3. user continues typing fun, completion item stays unchanged, but cmp knows before expanding the snippets, it needs to clear the fun typed by user, to let snippet engine expand correctly.
  4. user types <cr>, cmp turn vim.schedule(fun) to vim.schedule() correctly, BUT, it calls vim.lsp.util.apply_text_edits before expanding, while the TextEdit looks like this:
cmp stage before { "vim.schedule()" } -- This is before apply text edit
function#function#if completion_item.textEdit: {
  _index = 1,
  newText = "",
  range = {
    ["end"] = {
      character = 15, 
      line = 765
    },
    start = {
      character = 13,
      line = 765
    }
  }
}
cmp stage after { "vim.schedule(" } -- This is after appying

notice the range is [13,15) which stands for fn in the original placeholder.

vim.snippet.expand works correctly because it stays in select mode, does not trigger completion at all, only after user typing fun lua_ls will begin to send correct TextEdits, anyway, by that time placeholder is already gone.

To my understanding, the fix would be suspending completion request to stop step 2 from firing when first expanding.

@echasnovski
Copy link
Owner Author

...
vim.snippet.expand works correctly because it stays in select mode, does not trigger completion at all, only after user typing fun lua_ls will begin to send correct TextEdits, anyway, by that time placeholder is already gone.

To my understanding, the fix would be suspending completion request to stop step 2 from firing when first expanding.

Thanks for such a deep investigation! From the looks of it, is it safe to say that the issue is on 'nvim-cmp' side? The reason it gets noticeable with 'mini.snippets' and not vim.snippet.expand / 'L3MON4D3/LuaSnip' is because the first doesn't use Select mode, correct?

If that's the case, could you, maybe, create an issue in 'hrsh7th/nvim-cmp' with reproduction steps?

@xzbdmw
Copy link
Contributor

xzbdmw commented Dec 25, 2024

Sure, I’m thinking how cmp can get the information to not send request, currently it checks if current mode is insert mode to decide that.

Or, cmp needs to update outdated lsp response in some way, either lua_ls/cmp is to be blame…

@echasnovski
Copy link
Owner Author

I find "c-n" does not work given the config, it delete all the chars before cursor pos, after I expand and then use "right" to move one char, "c-n" works.

So I indeed tried to work around it, but there is no real solution here. For at least two reasons (in addition to dealing with some quirks):

  • The 'mini.snippets' uses complete(..., []) Vimscript call to hide completion. This is currently the best way to (immediately) hide current completions. The downside is that it still makes Neovim think that there is built-in completion going on, thus <C-n> and <C-p> are forced to be treated as "go to next/previous completion item". This doesn't make much sense to me (as completion list is empty) and I've asked around about the possibility of fully stopping completion in that case. Turns out, similar version of this was already discussed in Vim with the resolution "Let's not pollute this function and instead create another one". I think making a PR to Vim with new pumstop() function is possible, but it won't fix the issue number two.
  • If there is tabstop with choices, built-in completion menu is shown automatically. This means that in that case pressing <C-n> and <C-p> will not jump to next/previous tabstop but go to next/previous completion item. This is a usability problem so much so that I don't think accounting for this case is worth it.

So I indeed opted for a separate note about not using <C-n> and <C-p> in mappings.

@abeldekat
Copy link

Hello @echasnovski,

When a snippet is inserted and the user presses <esc>, the snippet restarts when entering insert mode again.
However, if the user deletes the snippet the last tabstop symbol is not removed and appears in a wrong place.
I know this can be avoided by using <c-c> instead of <esc>. Pressing <esc> is something I will have a hardtime to unlearn...

To reproduce, see the following repro:

init.lua
--[[
Use:
  mkdir ~/.config/repro
  cd ~/.config/repro

  touch init.lua
  add the contents of this file to init.lua
  NVIM_APPNAME=repro nvim init.lua 

Remove:
  rm -rf ~/.local/share/repro ~/.local/state/repro ~/.local/cache/repro
  rm -rf ~/.config/repro
--]]

--[[
Steps to reproduce:
  1. Navigate to one line above the line containing function clone
  2. Insert mode, type "for" and "<c-j>": The "for" snippet is expanded
  3. Type "esc" and "3dd"
  4. Now the final tabstop symbol is visible as the first character on the line
     containing function clone
  5. I cannot remove the symbol. Reloading the buffer also does not remove the symbol
  6. Restart nvim: The symbol is no longer present
--]]

local function clone(path_to_site)
  local mini_path = path_to_site .. "pack/deps/start/mini.nvim"
  if not vim.uv.fs_stat(mini_path) then
    vim.cmd('echo "Installing `mini.nvim`" | redraw')
    local clone_cmd =
      { "git", "clone", "--filter=blob:none", "https://github.com/echasnovski/mini.nvim", mini_path }
    vim.fn.system(clone_cmd)
    vim.cmd("packadd mini.nvim | helptags ALL")
    vim.cmd('echo "Installed `mini.nvim`" | redraw')
  end
end

local path_to_site = vim.fn.stdpath("data") .. "/site/"
clone(path_to_site)
local MiniDeps = require("mini.deps")
MiniDeps.setup({ path = { package = path_to_site } })
local add, now = MiniDeps.add, MiniDeps.now

now(function()
  add("rafamadriz/friendly-snippets")

  vim.cmd("colorscheme randomhue")
  require("mini.basics").setup() -- sensible defaults

  local gen_loader = require("mini.snippets").gen_loader
  require("mini.snippets").setup({
    snippets = {
      gen_loader.from_lang(),
    },
  })
  local mini_pick = require("mini.pick")
  mini_pick.setup()
  vim.ui.select = mini_pick.ui_select
end)

@echasnovski
Copy link
Owner Author

echasnovski commented Dec 27, 2024

When a snippet is inserted and the user presses <esc>, the snippet restarts when entering insert mode again.

It does not "restart" because snippet session never stopped. Snippet session stops only in two ways: manually or automatically. Exiting into Normal mode will stop session only if current tabstop is final, otherwise it is treated as an "exit for quick text edit and then go back to editing in snippet session".

However, if the user deletes the snippet the last tabstop symbol is not removed and appears in a wrong place.

The answer is simple: don't do that. I explored the possibility of stopping session whenever any tabstop extmark is invalidated, but that looked impossible because I want users to be able to delete tabstop text completely (i.e. for it to become empty string in order to be visualized as inline virtual text).

I know this can be avoided by using <c-c> instead of <esc>. Pressing <esc> is something I will have a hardtime to unlearn...

The important thing to understand here is that "stop snippet session" and "exit into Normal mode" are independent actions (if current tabstop is not final). If you want to stop the session whenever you press <Esc>, there are at least two ways to do that:

  • Use <Esc> as config.mappings.stop. It will be created only for the duration of active session(s). First press(es) will stop session(s) (and keep Insert mode) and it will exit Normal mode after there is no active session(s).
  • Set up ModeChanged *:n autocommand creation whenever session starts. With something like this:
    local modechanged_au_id
    local stop_all_sessions = function()
      -- Stop all nested sessions
      while MiniSnippets.session.get() do
        MiniSnippets.session.stop()
      end
      modechanged_au_id = nil
    end
    local setup_stop_in_normal_mode = function()
      if modechanged_au_id ~= nil then return end
      modechanged_au_id =
        vim.api.nvim_create_autocmd('ModeChanged', { pattern = '*:n', once = true, callback = stop_all_sessions })
    end
    vim.api.nvim_create_autocmd('User', { pattern = 'MiniSnippetsSessionStart', callback = setup_stop_in_normal_mode })
  1. I cannot remove the symbol. Reloading the buffer also does not remove the symbol

Yes, you can: enter Insert mode and press <C-c> to explicitly stop session.

@abeldekat
Copy link

Thank you! As always, your answer provides a better understanding of the plugin.

I really like mini.snippets!

In my config the plugin has been integrated with nvim-cmp to expand lsp snippets.
I have one problem though: I already use <c-j> for cmp.mapping.confirm({ select = true }) because typing <c-y> is more cumbersome to me.

For the moment I use mappings = { expand = "<C-S>" } but this is also not ideal due to a restriction in my left hand. If you have any advice that would be greatly appreciated.

@echasnovski
Copy link
Owner Author

For the moment I use mappings = { expand = "<C-S>" } but this is also not ideal due to a restriction in my left hand. If you have any advice that would be greatly appreciated.

Here are several options:

  • Use <CR> as "confirm completion item selection" mapping. This is fairly common, I'd guess.
  • Use the same key in "expand or jump" style. Similar to this example. I've personally grown to not like that (I used <C-l> to "expand or jump next") in favor of keeping those separate, but maybe you'll like it.
  • The <C-]> is also a reasonable key to expand snippet, but it is even more cumbersome to type.
  • If you don't want the default behavior of <C-k> in Insert mode, you can try it for expanding. And maybe remapping some other key so serve as <C-k> in Insert mode.

@krovuxdev
Copy link

Hi,
Thank you for your work on mini.snippet. I have a suggestion to improve the usability of the plugin.

Would it be possible to add a keymap, such as expand_choice = <C-E>, to allow users to select from predefined options in a choice snippet? This could be similar to how Choice works in Visual Studio Code snippets.

This functionality would streamline workflows for users who rely on choice-based snippets.

@echasnovski
Copy link
Owner Author

FYI: LazyVim PR

Thanks for the effort! Left couple of review comments which I think are relevant.

@abeldekat
Copy link

In the help, regarding "supertab", you write:

This module intentionally by default uses separate keys to expand and jump as
it enables cleaner use of nested sessions. Here is an example of setting up
custom to "expand or jump" and to "jump to previous": >lua

The word "cleaner" triggered me to think about concrete drawbacks.

In my config I can combine combinations of nvim-cmp, blink.cmp, luasnip, mini.snippets and vim.snippets.
I also added "supertab" to the config

Apart from not being ubiqitous and thus a little harder to navigate, I found one concrete drawback: Pressing tab at the start of the line or inside an empty tabstop inserts a real "tab" instead of opening the somewhat expected vim.ui.select containing all relevant snippets.
Are there other drawbacks? Would it be a good idea to materialize them in the help?

The suggested implementation of supertab in the help:

  -- ...
  local snippets = require('mini.snippets')
  local match_strict = function(snippets)
    -- Do not match with whitespace to cursor's left
    return snippets.default_match(snippets, { pattern_fuzzy = '%S+' })
  end
  -- ...

This introduces warning: "Redefined local snippets" and error E5108: Error executing lua: .../.config/nvim/lua/ak/config/coding/mini_snippets.lua:53: attempt to call field 'default_match' (a nil value)

I would suggest the following:

  -- ...
  local snippets = require('mini.snippets')
  local match_strict = function(candidate_snippets)
    -- Do not match with whitespace to cursor's left
    return snippets.default_match(candidate_snippets, { pattern_fuzzy = '%S+' })
  end
  -- ...

Would you be willing to accept a PR adding the candidate_snippets fragment to the help?

@echasnovski
Copy link
Owner Author

echasnovski commented Dec 30, 2024

Pressing tab at the start of the line or inside an empty tabstop inserts a real "tab" instead of opening the somewhat expected vim.ui.select containing all relevant snippets.
Are there other drawbacks? Would it be a good idea to materialize them in the help?

This is a result pattern_fuzzy = '%S+' and was deliberate because I'd expect that pressing "tab" after a whitespace (or at the start of line) to insert a real "tab". Otherwise the key is pretty much reserved for snippets and inserting a real "tab" requires <C-v><Tab>, which I don't think ideal. Removing , { pattern_fuzzy = '%S+' } altogether in the user's code should fix this.

Would you be willing to accept a PR adding the candidate_snippets fragment to the help?

That's my oversight of not sticking exactly to the example in the tests. I am currently working on the module, so will fix it myself (with you as co-author, of course :) ). Thanks for noticing!

Edit: db8790f

@abeldekat
Copy link

I intend to write a cmp resource: cmp_mini_snippets.

While investigating I saw that LuaSnip supports "autosnippets". Perhaps this feature could be added to the comparisons section of the help?

@echasnovski
Copy link
Owner Author

I intend to write a cmp resource: cmp_mini_snippets.

That's great! Let me know if you need any clarifications and/or 'mini.snippets' adjustment. It would also be best to possibly move that discussion somewhere else. Do you plan to have separate repo for that from the start? If yes, then maybe there; if not, then in separate discussion here.

While investigating I saw that LuaSnip supports "autosnippets". Perhaps this feature could be added to the comparisons section of the help?

Ah, you mean enable_autosnippets config? Yeah, didn't notice that. Makes sense to add to comparison. I'll do it today.

@abeldekat
Copy link

Very much WIP: https://github.com/abeldekat/cmp-mini-snippets

@xzbdmw
Copy link
Contributor

xzbdmw commented Dec 31, 2024

Stay in insert mode is superior, no longer need to escape select mode when jump to previous tabstop! I'm looking to write a cmp-minisnippets.

I've made a simple one, mainly to integrate my pr. The main branch has incorrect indent for multiline ghost text.

It does not do any smart-loading or caching, I feel this is unnecessary, seems mini.snippets already handles it quit well;)

It looks like this

iShot_2025-01-01_00.19.38.mp4

@echasnovski
Copy link
Owner Author

Initially it was to account for the fact that user can type some "partial" text and go back later to it. But now I have doubts about if it is a good idea. I'll investigate if changing it to always show all choices makes more sense or making a more clear documentation is better here.

@krovuxdev, I believe the issue should now be resolved: all choices are shown after jumping and deleting all tabstop text. There is also now more info about interaction with choices.

@krovuxdev
Copy link

Hi @echasnovski,

Thank you very much! It worked, but I noticed a small problem.
When I don't want to show the menu while expanding the snippet and press <C-n>, the menu still appears. This shouldn't happen. I showed the issue in the attached video.

completeopt.mp4

@echasnovski
Copy link
Owner Author

When I don't want to show the menu while expanding the snippet and press <C-n>, the menu still appears. This shouldn't happen. I showed the issue in the attached video.

As far as I can tell, the menu appears only after the initial insert and all other times it works as expected, correct? The reason for that is because MiniSnippetsSessionStart is triggered right after a session is started which is a common way to trigger any Enter/Start type of events. This also means that choices for the first tabstop are already shown at the point that event is triggered. If you want the menu not shown even in this situation, then moving the logic of adjusting 'completeopt' into custom config.expand.insert() (i.e. before calling MiniSnippets.default_insert()) is the way to go here.

Other than that, the general direction of not showing completion menu by adjusting 'completeopt' is indeed the suggested approach here. Couple of questions/suggestions, though:

  • The choices suggestions work best when there is a noselect flag there. So I'd change the nomenu case to 'noselect'. But it should still work with other 'completeopt' values, just won't be tested for them.
  • I am not sure why third type of adjustment in MiniSnippetsSessionJump is needed. Was there a particular reason for it to be added?

@9seconds
Copy link

9seconds commented Jan 3, 2025

@echasnovski this is a first snippet plugin in any editor I've tried that works exactly how I want: sometimes during snippet session I have to check something, like open vertical split etc, and continue with snippet after. Very huge thanks for that!

I have only one suggestion: the way of adding custom variables is not very obvious and documented. This is what I'm doing now (with lazy.nvim):

{
  opts = function()
    local plugin = require("mini.snippets")

    return {
      expand = {
        insert = function(snippet, opts)
          return plugin.default_insert(snippet, vim.tbl_deep_extend("force", opts or {}, {
            lookup = {
              ["GIT_USERNAME"] = vim.fn.system("git config user.name")
            }
          }))
        end
      }
    }
  end
}

This works. But I guess, such case would be very frequent so ideally, I would like to have nice shortcuts just to insert such variables. Do you think it makes sense to expose it to configuration?

@echasnovski
Copy link
Owner Author

This works. But I guess, such case would be very frequent so ideally, I would like to have nice shortcuts just to insert such variables. Do you think it makes sense to expose it to configuration?

Adding an example with exactly this approach, which is indeed a suggested one, to variable lookup is planned (didn't realize I missed it).

Regarding this exact GIT_USERNAME use case I'd suggest creating it as an environment variable in the current Neovim session, like somewhere in the 'init.lua'. Environment variables are recognized during snippet parsing and normalization. Does it count as a "nice shortcut"?

@9seconds
Copy link

9seconds commented Jan 3, 2025

@echasnovski you are right about environment variable in this case. I just wanted to make some simple illustration without huge piles of specific code

@echasnovski
Copy link
Owner Author

I just wanted to make some simple illustration without huge piles of specific code

I don't think it is a huge pile. The vim.tbl_deep_extend is not needed here, as config.expand.insert accepts only a snippet as argument. The opts table is an argument for default_insert() exactly for this occasion of tweaking its options.

So it would be something like this:

-- Use evnironment variables with value is same for all snippet sessions
vim.loop.os_setenv('USERNAME', 'user')

-- Compute custom lookup for variables with dynamic values
local insert_with_lookup = function(snippet)
  local lookup = { TM_SELECTED_TEXT = vim.fn.getreg('*') }
  return MiniSnippets.default_insert(snippet, { lookup = lookup })
end

require('mini.snippets').setup({
  -- ... Set up snippets ...
  expand = { insert = insert_with_lookup },
})

@9seconds
Copy link

9seconds commented Jan 3, 2025

Seems I've found a bug. Consider a following snippet for Python:

return {
  ["'try/except' clause"] = {
    prefix = "te",
    body = {
      "try:",
      "\t${0:$TM_SELECTED_TEXT}",
      "except Exception:",
      "\tpass",
    },
  },
}

CleanShot 2025-01-03 at 14 22 26

The problem is that in case of multiline $TM_SELECTED_TEXT only first line is correctly indented so I have to reindent.

I've seen something relevant here: hrsh7th/vim-vsnip#86 Hope it helps 🙏

@echasnovski
Copy link
Owner Author

The problem is that in case of multiline $TM_SELECTED_TEXT only first line is correctly indented so I have to reindent.

Hmmm... My initial impression is that the current behavior is correct. Resolving variables is best understood as a straightforward replace. In this case it is replaced with 'multi\nline\ntext' which should be treated as if it was used in the snippet to begin with. I do get that it might be useful the other way around, but still.

Tweaking indent during variable expansion also opens a big Pandora box. For example, the case of several such variables together. Like in case of '\t$TM_SELECTED_TEXT$TM_SELECTED_TEXT' where selected text itself adds indent at its last line.

The other technical reason is that indents in 'mini.snippets' respect comment leaders which might introduce problems.

I'll think about the concise way to possibly adjust for that, but at the moment I am a bit skeptical.

@radmen
Copy link

radmen commented Jan 3, 2025

Hello. Thank you very much for the plugin. I think this is exactly what I wanted.

I tried to scoop the thread and see if this was mentioned something. Apologies if I duplicated the reports.

  1. is there a quick way to remove/blank the placeholder value? Sometimes I add them as hints, but would like to be able to quickly remove them if I want to keep the value empty. Currently, I handle this by <Space><Backspace>
  2. Would it be possible to highlight the placeholder? I have a snippet with a nested variable and would like to see how "far" it reaches (mostly to be able to remove it if I don't need it).

Here is a short demo vid:

Kooha-2025-01-03-18-15-19.mp4

And the snippet used in the video:

  "function": {
    "prefix": "fun",
    "body": [
      "function (${2:arg})${3: use ($$4)} {",
      "\t$0",
      "}"
    ]
  },

PS: I just realized that I could just add a second snippet just for the (use ($$4))) part, though I still would find it useful if the things I mentioned are possible.

Thank you!

@echasnovski
Copy link
Owner Author

Hello. Thank you very much for the plugin. I think this is exactly what I wanted.

🎉

1. is there a quick way to remove/blank the placeholder value? Sometimes I add them as hints, but would like to be able to quickly remove them if I want to keep the value empty. Currently, I handle this by `<Space><Backspace>`

I also encountered it, but I am afraid the current approach of "remove placeholder after typing at its start" is the best design here. Mostly because placeholders (in my opinion) should contain "valid" text in the context of the snippet (for programming languages - something that syntactically correct, for example). Or more strictly they are "leave it as is or replace" type of text. So using <Space><BS> is the way here.

2. Would it be possible to highlight the placeholder? I have a snippet with a nested variable and would like to see how "far" it reaches (mostly to be able to remove it if I don't need it).

It is highlighted. Depending on the tabstop and session progression, tabstops are highlighted with one of five highlight groups. Those by default are colored underdouble which is not really seen in your screencast. Check out demo to see how they can be seen. Or define are more visible highlight groups for your terminal/color scheme. Here is a crude quick example:

vim.api.nvim_set_hl(0, 'MiniSnippetsCurrent', { bg = 'Yellow' })
vim.api.nvim_set_hl(0, 'MiniSnippetsCurrentReplace', { bg = 'Red' })
vim.api.nvim_set_hl(0, 'MiniSnippetsFinal', { bg = 'Green' })
vim.api.nvim_set_hl(0, 'MiniSnippetsUnvisited', { bg = 'Cyan' })
vim.api.nvim_set_hl(0, 'MiniSnippetsVisited', { bg = 'Blue' })

@radmen
Copy link

radmen commented Jan 3, 2025

Mostly because placeholders (in my opinion) should contain "valid" text in the context of the snippet

Yeah, I think you're right on this one. It hit me once I started thinking in possible workarounds for my issues.

It is highlighted. Depending on the tabstop and session progression, tabstops are highlighted with one of five highlight groups

Oh, got it. So it's more an issue of my color theme.

Thank you!

@echasnovski
Copy link
Owner Author

The problem is that in case of multiline $TM_SELECTED_TEXT only first line is correctly indented so I have to reindent.

Hmmm... My initial impression is that the current behavior is correct. Resolving variables is best understood as a straightforward replace. In this case it is replaced with 'multi\nline\ntext' which should be treated as if it was used in the snippet to begin with. I do get that it might be useful the other way around, but still.

@9seconds, after quick testing I agree that having some kind of indent adjustments in case of expanding variables is more expected. Probably, nested placeholders also (like in $1\n${2:\t$1} or $1\n\t${2:$1}). I'll have to do a research and think about how to reasonably achieve this.

I have only one suggestion: the way of adding custom variables is not very obvious and documented. This is what I'm doing now (with lazy.nvim):

Also, this is now documented.

@abeldekat
Copy link

I submitted a PR in blink.cmp.

For caching, I saw the context you mentioned earlier. I copied H.get_default_context and modified the return value to be only lang. Is there a reason you are using the buf_id/lang combination?

@krovuxdev
Copy link

When I don't want to show the menu while expanding the snippet and press <C-n>, the menu still appears. This shouldn't happen. I showed the issue in the attached video.

As far as I can tell, the menu appears only after the initial insert and all other times it works as expected, correct? The reason for that is because MiniSnippetsSessionStart is triggered right after a session is started which is a common way to trigger any Enter/Start type of events. This also means that choices for the first tabstop are already shown at the point that event is triggered. If you want the menu not shown even in this situation, then moving the logic of adjusting 'completeopt' into custom config.expand.insert() (i.e. before calling MiniSnippets.default_insert()) is the way to go here.

Other than that, the general direction of not showing completion menu by adjusting 'completeopt' is indeed the suggested approach here. Couple of questions/suggestions, though:

* The choices suggestions work best [when there is a `noselect`](https://github.com/echasnovski/mini.nvim/blob/782969a40d2870b1d7cd7c9db8f965d8aa9705ef/doc/mini-snippets.txt#L994) flag there. So I'd change the `nomenu` case to 'noselect'. But it should still work with other 'completeopt' values, just won't be tested for them.

* I am not sure why third type of adjustment in `MiniSnippetsSessionJump` is needed. Was there a particular reason for it to be added?

Thank you very much, it worked with config.expand.insert instead of MiniSnippetsSessionStart.

* I am not sure why third type of adjustment in `MiniSnippetsSessionJump` is needed. Was there a particular reason for it to be added?

I was trying to prevent text from being overwritten or reset when using jumpPre or jump with noinsert, but you already gave me a solution, and the config.expand.insert with noselect worked. That was it.

I noticed that when I change the choices, there are three options like "one", "two", "three". Sometimes, when I press <C-n>, it selects "three" twice, and in the next one, it behaves as if there were four options. This is probably caused by noinsert or noselect, but still, thank you very much, it helped me.

@echasnovski
Copy link
Owner Author

I copied H.get_default_context and modified the return value to be only lang.

Please, don't do that. There is a documented way of getting default context.

Is there a reason you are using the buf_id/lang combination?

Yes, because 'vim.b.minisnippets_config` can contain buffer-local snippets. And that should be respected.

But that is not the biggest issue here as users can use their own context values for a more granular evaluation of loaders. So this type of caching can be actively bad.

@krovuxdev
Copy link

@echasnovski, I have a question: is there a way to do something like GIT:USERNAME to get the git username?

But neither of the two options works, only GIT_USERNAME works.

lookup = {
            GIT_USERNAME = vim.fn.system("git config user.name"), -- works
            -- ["GIT:USERNAME"] = vim.fn.system("git config user.name"), doesn't work
            GIT = {
                USERNAME = vim.fn.system("git config user.name"),
            },
        },

global.json

         "name": {
        "prefix": "git_username",
        "body": [
            "user: ${GIT:USERNAME}"
        ],
        "description": " git username"
    }

(I like the structure where GIT = {USERNAME, EMAIL, ETC}. It’s very convenient)

@echasnovski
Copy link
Owner Author

@echasnovski, I have a question: is there a way to do something like GIT:USERNAME to get the git username?

No, it is not possible. Variable names must match the [_a-zA-Z] [_a-zA-Z0-9]* regular expression (i.e. start with Latin letter or underscore and contain only Latien letters, digits, or underscore), which is described in specification grammar.

Nested lookup is also not allowed, as documented.

@echasnovski
Copy link
Owner Author

Seems I've found a bug. Consider a following snippet for Python:

return {
  ["'try/except' clause"] = {
    prefix = "te",
    body = {
      "try:",
      "\t${0:$TM_SELECTED_TEXT}",
      "except Exception:",
      "\tpass",
    },
  },
}

...
The problem is that in case of multiline $TM_SELECTED_TEXT only first line is correctly indented so I have to reindent.

Variables now should preserve relative indent on main branch. Thankfully, the actual implementation was relatively straightforward thanks to one small decision in parse() design which I contemplated for several days (to preserve all data about nodes during normalization and just add text field or collapse them into text nodes with fewer nodes in total).

The $TM_SELECTED_TEXT now also works better with linewise selections (no extra new line at the end; this seems to be more useful in practice).

I postponed the same change for linked tabstops (like $1\n\t$1 and $1\n${2:\t$1}), as it is not clear if it is a good idea and implementation also might be involved.

@echasnovski
Copy link
Owner Author

I've pushed an updated for linked tabstops to preserve relative indent. It is easier to demonstrate what it means:

minisnippets_relative-indent-in-linked-tabstops.mp4

I have a bad feeling that this might not account for some weird edge cases or that it is not wanted as a feature. But it seems natural to handle tabstop text in the same way as variable's text (which is indeed useful).

Thanks again, @9seconds, for the general idea of "preserve relative indent".

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

No branches or pull requests

7 participants