Skip to content

chrisgrieser/nvim-scissors

Repository files navigation

nvim-scissors ✂️

badge

Automagical editing and creation of snippets.

add-new-snippet.mp4
edit-existing-snippet.mp4

Table of contents

Features

  • Add new snippets, edit snippets, or delete snippets on the fly.
  • Syntax highlighting while you edit the snippet. Includes highlighting of tabstops and placeholders such as $0, ${2:foobar}, or $CLIPBOARD
  • Automagical conversion from buffer text to JSON string.
  • Intuitive UI for editing the snippet, dynamically adapting the number of prefixes.
  • Automatic hot-reloading of any changes, so you do not have to restart nvim for changes to take effect.
  • Optional JSON-formatting and sorting of the snippet file. (Useful when version-controlling your snippet collection.)
  • Snippet/file selection via telescope or vim.ui.select.
  • Automatic bootstrapping of the snippet folder or new snippet files if needed.
  • Supports only VSCode-style snippets.

Tip

You can use snippet-converter.nvim to convert your snippets to the VSCode format.

Rationale

  • The VSCode snippet format is the closest thing to a standard regarding snippets. It is used by friendly-snippets and supported by most snippet engine plugins for nvim.
  • However, VSCode snippets are stored as JSON, which are a pain to modify manually. This plugin alleviates that pain by automagically writing the JSON for you.

Requirements

Installation

nvim-scissors

-- lazy.nvim
{
	"chrisgrieser/nvim-scissors",
	dependencies = "nvim-telescope/telescope.nvim", 
	opts = {
		snippetDir = "path/to/your/snippetFolder",
	} 
},

-- packer
use {
	"chrisgrieser/nvim-scissors",
	dependencies = "nvim-telescope/telescope.nvim", 
	config = function()
		require("scissors").setup ({
			snippetDir = "path/to/your/snippetFolder",
		})
	end,
}

Snippet engine setup

In addition, your snippet engine needs to point to the same snippet folder as nvim-scissors:

Tip

vim.fn.stdpath("config") returns the path to your nvim config.

LuaSnip

require("luasnip.loaders.from_vscode").lazy_load {
	paths = { "path/to/your/snippetFolder" },
}

mini.snippets

mini.snippets preferred snippet location is any snippets/ directory in the runtimepath. For manually maintained snippets the best location is the user config directory, which requires the following nvim-scissors setup:

require("scissors").setup({
	snippetDir = vim.fn.stdpath("config") .. "/snippets",
})

The mini.snippets setup requires explicit definition of loaders. Following its Quickstart guide should be enough to make it respect snippets from 'snippets/' directory inside user config. Note: nvim-scissors works only with VSCode-style snippet files (not Lua files or JSON arrays), and also requires a package.json for the VSCode format.

blink.cmp

require("blink.cmp").setup {
	sources = {
		providers = {
			snippets = {
				opts = {
					search_paths = { "path/to/your/snippetFolder" },
				},
			}
		}
	}
}

It is recommended to use the latest release of blink.cmp for hot-reloading to work.

basics-language-server

-- NOTE: this requires `nvim-lspconfig` as additional dependency
require("lspconfig").basics_ls.setup({
    settings = {
        snippet = {
            enable = true,
            sources = { "path/to/your/snippetFolder" }
        },
    }
})

Note that Hot-reloading of the new/edited snippet for basics_ls also requires nvim-lspconfig.

nvim-snippets

require("nvim-snippets").setup {
	search_paths = { "path/to/your/snippetFolder" },
}

vim-vsnip

vim.g.vsnip_snippet_dir = "path/to/your/snippetFolder"
-- OR
vim.g.vsnip_snippet_dirs = { "path/to/your/snippetFolder" }

Usage

Starting nvim-scissors

The plugin provides two lua functions, .addNewSnippet() and .editSnippet():

vim.keymap.set(
	"n",
	"<leader>se",
	function() require("scissors").editSnippet() end,
	{ desc = "Snippet: Edit" }
)

-- when used in visual mode, prefills the selection as snippet body
vim.keymap.set(
	{ "n", "x" },
	"<leader>sa",
	function() require("scissors").addNewSnippet() end,
	{ desc = "Snippet: Add" }
)

You can also use :ScissorsAddNewSnippet and :ScissorsEditSnippet if you prefer ex commands.

The :ScissorsAddSnippet ex command also accepts a range to prefill the snippet body (for example :'<,'> ScissorsAddSnippet or :3 ScissorsAddSnippet).

Editing snippets in the popup window

The popup is just one window, so you can move between the prefix area and the body with j and k or any other movement command. ("Prefix" is how trigger words are referred to in the VSCode format.)

Use showHelp (default keymap: ?) to show a notification containing all keymaps.

The popup intelligently adapts to changes in the prefix area: Each line represents one prefix, and creating or removing lines in that area thus changes the number of prefixes.

Showcase prefix change

Configuration

The .setup() call is optional.

-- default settings
require("scissors").setup {
	snippetDir = vim.fn.stdpath("config") .. "/snippets",
	editSnippetPopup = {
		height = 0.4, -- relative to the window, between 0-1
		width = 0.6,
		border = "rounded",
		keymaps = {
			-- if not mentioned otherwise, the keymaps apply to normal mode
			cancel = "q",
			saveChanges = "<CR>", -- alternatively, can also use `:w`
			goBackToSearch = "<BS>",
			deleteSnippet = "<C-BS>",
			duplicateSnippet = "<C-d>",
			openInFile = "<C-o>",
			insertNextPlaceholder = "<C-p>", -- insert & normal mode
			showHelp = "?",
		},
	},
	telescope = {
		-- By default, the query only searches snippet prefixes. Set this to
		-- `true` to also search the body of the snippets.
		alsoSearchSnippetBody = false,

		-- accepts the common telescope picker config
		opts = {
			layout_strategies = "horizontal",
			layout_config = {
				horizontal = { width = 0.9 },
				preview_width = 0.6,
			},
		},
	},

	-- `none` writes as a minified json file using `vim.encode.json`.
	-- `yq`/`jq` ensure formatted & sorted json files, which is relevant when
	-- you version control your snippets. To use a custom formatter, set to a
	-- list of strings, which will then be passed to `vim.system()`.
	---@type "yq"|"jq"|"none"|string[]
	jsonFormatter = "none",

	backdrop = {
		enabled = true,
		blend = 50, -- between 0-100
	},
	icons = {
		scissors = "󰩫",
	},
}

Cookbook & FAQ

Introduction to the VSCode-style snippet format

This plugin requires that you have a valid VSCode snippet folder. In addition to saving the snippets in the required JSON format, there must also be a package.json file at the root of the snippet folder, specifying which files should be used for which languages.

Example file structure inside the snippetDir:

.
├── package.json
├── python.json
├── project-specific
│   └── nvim-lua.json
├── javascript.json
└── allFiletypes.json

Example package.json:

{
	"contributes": {
		"snippets": [
			{
				"language": "python",
				"path": "./python.json"
			},
			{
				"language": "lua",
				"path": "./project-specific/nvim-lua.json"
			},
			{
				"language": ["javascript", "typescript"],
				"path": "./javascript.json"
			},
			{
				"language": "all",
				"path": "./allFiletypes.json"
			}
		]
	},
	"name": "my-snippets"
}

Note

The special filetype all enables the snippets globally, regardless of filetype.

Example snippet file (here: nvim-lua.json):

{
  "autocmd (Filetype)": {
    "body": [
      "vim.api.nvim_create_autocmd(\"FileType\", {",
      "\tpattern = \"${1:ft}\",",
      "\tcallback = function()",
      "\t\t$0",
      "\tend,",
      "})"
    ],
    "prefix": "autocmd (Filetype)"
  },
  "file exists": {
    "body": "local fileExists = vim.uv.fs_stat(\"${1:filepath}\") ~= nil",
    "prefix": "file exists"
  },
}

For details, read the official VSCode snippet documentation:

Tabstops and variables

Tabstops are denoted by $1, $2, $3, etc., with $0 being the last tabstop. They support placeholders such as ${1:foobar}.

Note

Due to the use of $ in the snippet syntax, any literal $ needs to be escaped as \$.

Furthermore, there are various variables you can use, such as $TM_FILENAME or $LINE_COMMENT. See here for a full list of variables.

Tip

If you frequently create new snippets, you can also use the command :ScissorsCreateSnippetsForSnippetVars to create snippets for the VSCode snippet variables in the nvim-scissors popup window (i.e., snippets for creating snippets). For example, typing filen will then trigger a suggestion for $TM_FILENAME.

friendly-snippets

Even though the snippets from the friendly-snippets repository are written in the VSCode-style format, editing them directly is not supported. The reason being that any changes made would be overwritten as soon as the friendly-snippets repository is updated (which happens fairly regularly). Unfortunately, there is little nvim-scissors can do about that.

What you can do, however, is to copy individual snippets files from the friendly-snippets repository into your own snippet folder, and edit them there.

Edit snippet title or description

nvim-scissors only allows to edit the snippet prefix and snippet body, to keep the UI as simple as possible. For the few cases where you need to edit a snippet's title or description, you can use the openInFile keymap and edit them directly in the snippet file.

Version controlling snippets & snippet file formatting

This plugin writes JSON files via vim.encode.json(). That method saves the file in minified form and does not have a deterministic order of dictionary keys.

Both, minification and unstable key order, are a problem if you version-control your snippet collection. To solve this issue, nvim-scissors lets you optionally unminify and sort the JSON files via yq or jq after updating a snippet. (Both are also available via mason.nvim.)

It is recommended to run yq/jq once on all files in your snippet collection, since the first time you edit a file, you would still get a large diff from the initial sorting. You can do so with yq using this command:

cd "/your/snippet/dir"
fd ".*\.json" | xargs -I {} yq --inplace --output-format=json "sort_keys(..)" {}

How to do the same with jq is left as an exercise to the reader.

Snippets on visual selections (Luasnip only)

With Luasnip, this is an opt-in feature, enabled via:

require("luasnip").setup {
	store_selection_keys = "<Tab>",
}

In your VSCode-style snippet, use the token $TM_SELECTED_TEXT at the location where you want the selection to be inserted. (It's roughly the equivalent of LS_SELECT_RAW in the Luasnip syntax.)

Then, in visual mode, press the key from store_selection_keys. The selection disappears, and you are put in insert mode. The next snippet you now trigger is going to have $TM_SELECTED_TEXT replaced with your selection.

Auto-triggered snippets (Luasnip only)

While the VSCode snippet format does not support auto-triggered snippets, LuaSnip allows you to specify auto-triggering in the VSCode-style JSON files by adding the luasnip key.

nvim-scissors does not touch any keys other than prefix and body in the JSON files, so any additions like the luasnip key are preserved.

Tip

You can use the openInFile keymap to directory open JSON file at the snippet's location to make edits there easier.

About the author

In my day job, I am a sociologist studying the social mechanisms underlying the digital economy. For my PhD project, I investigate the governance of the app economy and how software ecosystems manage the tension between innovation and compatibility. If you are interested in this subject, feel free to get in touch.

I also occasionally blog about vim: Nano Tips for Vim

Buy Me a Coffee at ko-fi.com