Skip to content
Carlo Sala edited this page Oct 10, 2023 · 15 revisions

Hint node-type with virtual text

Using ext_opts it's possible to add virtual text to nodes:

local types = require("luasnip.util.types")

require'luasnip'.config.setup({
	ext_opts = {
		[types.choiceNode] = {
			active = {
				virt_text = {{"", "GruvboxOrange"}}
			}
		},
		[types.insertNode] = {
			active = {
				virt_text = {{"", "GruvboxBlue"}}
			}
		}
	},
})

This adds an orange dot to the end of the line if inside a choiceNode, and a blue one if inside an insertNode:

showcase

Imitate vscodes' behaviour for nested placeholders

local util = require("luasnip.util.util")
local node_util = require("luasnip.nodes.util")

ls.setup({
  parser_nested_assembler = function(_, snippetNode)
    local select = function(snip, no_move, dry_run)
      if dry_run then
        return
      end
      snip:focus()
      -- make sure the inner nodes will all shift to one side when the
      -- entire text is replaced.
      snip:subtree_set_rgrav(true)
      -- fix own extmark-gravities, subtree_set_rgrav affects them as well.
      snip.mark:set_rgravs(false, true)

      -- SELECT all text inside the snippet.
      if not no_move then
        vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", true)
        node_util.select_node(snip)
      end
    end

    local original_extmarks_valid = snippetNode.extmarks_valid
    function snippetNode:extmarks_valid()
      -- the contents of this snippetNode are supposed to be deleted, and
      -- we don't want the snippet to be considered invalid because of
      -- that -> always return true.
      return true
    end

    function snippetNode:init_dry_run_active(dry_run)
      if dry_run and dry_run.active[self] == nil then
        dry_run.active[self] = self.active
      end
    end

    function snippetNode:is_active(dry_run)
      return (not dry_run and self.active) or (dry_run and dry_run.active[self])
    end

    function snippetNode:jump_into(dir, no_move, dry_run)
      self:init_dry_run_active(dry_run)
      if self:is_active(dry_run) then
        -- inside snippet, but not selected.
        if dir == 1 then
          self:input_leave(no_move, dry_run)
          return self.next:jump_into(dir, no_move, dry_run)
        else
          select(self, no_move, dry_run)
          return self
        end
      else
        -- jumping in from outside snippet.
        self:input_enter(no_move, dry_run)
        if dir == 1 then
          select(self, no_move, dry_run)
          return self
        else
          return self.inner_last:jump_into(dir, no_move, dry_run)
        end
      end
    end

    -- this is called only if the snippet is currently selected.
    function snippetNode:jump_from(dir, no_move, dry_run)
      if dir == 1 then
        if original_extmarks_valid(snippetNode) then
          return self.inner_first:jump_into(dir, no_move, dry_run)
        else
          return self.next:jump_into(dir, no_move, dry_run)
        end
      else
        self:input_leave(no_move, dry_run)
        return self.prev:jump_into(dir, no_move, dry_run)
      end
    end

    return snippetNode
  end,
})

The main reason for this not being the default is that it

  • requires a lot of modification to the resulting snippets
  • Doesn't work perfectly(yet): if the entire placeholder is replaced, the nested tabstops won't be skipped.

Example with "test: ${1: this is ${2:nested} ${3:text}} ${4:yay!}": showcase

Only jump in the current snippet

A slight change to the recommended nvim-cmp setup where we replace luasnip.expand_or_jumpable() with luasnip.expand_or_locally_jumpable() to only jump to a snippet field if we are currently in a snippet. example setup:

cmp.setup({

  -- ... Your other configuration ...

  mapping = {

    -- ... Your other mappings ...

    ["<Tab>"] = cmp.mapping(function(fallback)
      if cmp.visible() then
        cmp.select_next_item()
      elseif luasnip.expand_or_locally_jumpable() then
        luasnip.expand_or_jump()
      elseif has_words_before() then
        cmp.complete()
      else
        fallback()
      end
    end, { "i", "s" }),
  }
}

"Super-Tab"-like setup for Change Choice and External Update Dynamic Node

Similar to the above, but this is for setting the same keymap for changing choice if there's an active choicenode or using external update dynamic node. Since there are a lot of keymaps already, it might be nice to overload them and use the same keymap for mutually exclusive events. Set it up as follows:

-- feel free to change the keys to new ones, those are just my current mappings
vim.keymap.set("i", "<C-f>", function ()
    if ls.choice_active() then
        return ls.change_choice(1)
    else
        return _G.dynamic_node_external_update(1) -- feel free to update to any index i
    end
end, { noremap = true })
vim.keymap.set("s", "<C-f>", function ()
    if ls.choice_active() then
        return ls.change_choice(1)
    else
        return _G.dynamic_node_external_update(1)
    end
end, { noremap = true })
vim.keymap.set("i", "<C-d>", function ()
    if ls.choice_active() then
        return ls.change_choice(-1)
    else
        return _G.dynamic_node_external_update(2)
    end
end, { noremap = true })
vim.keymap.set("s", "<C-d>", function ()
    if ls.choice_active() then
        return ls.change_choice(-1)
    else
        return _G.dynamic_node_external_update(2)
    end
end, { noremap = true })

Also see:

Clone this wiki locally