Skip to content

Latest commit

 

History

History
377 lines (280 loc) · 18.9 KB

mini-snippets.md

File metadata and controls

377 lines (280 loc) · 18.9 KB

GitHub license

Manage and expand snippets

See more details in Features and help file.


⦿ This is a part of mini.nvim library. Please use this link if you want to mention this module.

⦿ All contributions (issues, pull requests, discussions, etc.) are done inside of 'mini.nvim'.

⦿ See the repository page to learn about common design principles and configuration recipes.


If you want to help this project grow but don't know where to start, check out contributing guides of 'mini.nvim' or leave a Github star for 'mini.nvim' project and/or any its standalone Git repositories.

Demo

demo-snippets.mp4

Features

  • Manage snippet collection by adding it explicitly or with a flexible set of performant built-in loaders. See :h MiniSnippets.gen_loader.

  • Configured snippets are efficiently resolved before every expand based on current local context. This, for example, allows using different snippets in different local tree-sitter languages (like in markdown code blocks). See :h MiniSnippets.default_prepare().

  • Match which snippet to insert based on the currently typed text. Supports both exact and fuzzy matching. See :h MiniSnippets.default_match().

  • Select from several matched snippets via vim.ui.select(). See :h MiniSnippets.default_select().

  • Insert, jump, and edit during snippet session in a configurable manner:

    • Configurable mappings for jumping and stopping.
    • Jumping wraps around the tabstops for easier navigation.
    • Easy to reason rules for when session automatically stops.
    • Text synchronization of linked tabstops.
    • Dynamic tabstop state visualization (current/visited/unvisited, etc.)
    • Inline visualization of empty tabstops (requires Neovim>=0.10).
    • Works inside comments by preserving comment leader on new lines.
    • Supports nested sessions (expand snippet while there is an active one).

    See :h MiniSnippets.default_insert().

  • Exported function to parse snippet body into easy-to-reason data structure. See :h MiniSnippets.parse().

Notes:

  • It does not set up any snippet collection by default. Explicitly populate config.snippets to have snippets to match from.
  • It does not come with a built-in snippet collection. It is expected from users to add their own snippets, manually or with dedicated plugin(s).
  • It does not support variable/tabstop transformations in default snippet session. This requires ECMAScript Regular Expression parser which can not be implemented concisely.

Sources with more details:

  • Overview
  • :h MiniSnippets-glossary
  • :h MiniSnippets-examples
  • :h MiniSnippets-in-other-plugins (for plugin authors)

Dependencies

This module doesn't come with snippet collection. Either create it manually or install a dedicated plugin. For example, rafamadriz/friendly-snippets.

Quickstart

  • Use the following setup:

    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(),
      },
    })

    This setup allows having single file with custom "global" snippets (will be present in every buffer) and snippets which will be loaded based on the local language (see :h MiniSnippets.gen_loader.from_lang()).

    Create language snippets manually (like by creating and populating '$XDG_CONFIG_HOME/nvim/snippets/lua.json' file) or by installing dedicated snippet collection plugin (like rafamadriz/friendly-snippets).

    Note: all built-in loaders cache their output by default. It means that after a file is first read, changing it won't have effect during current Neovim session. See :h MiniSnippets.gen_loader about how to reset cache if necessary.

  • Open Neovim in a file with dedicated language (like 'init.lua' from your config) and press <C-j>.

The best way to grasp the design of snippet management and expansion is to try them out yourself. Here are extra steps for a basic demo:

  • Create 'snippets/global.json' file in the config directory with the content:

    {
      "Basic":        { "prefix": "ba", "body": "T1=$1 T2=$2 T0=$0"         },
      "Placeholders": { "prefix": "pl", "body": "T1=${1:aa}\nT2=${2:<$1>}"  },
      "Choices":      { "prefix": "ch", "body": "T1=${1|a,b|} T2=${2|c,d|}" },
      "Linked":       { "prefix": "li", "body": "T1=$1\nT1=$1"              },
      "Variables":    { "prefix": "va", "body": "Runtime: $VIMRUNTIME\n"    },
      "Complex":      {
        "prefix": "co",
        "body": [ "T1=${1:$RANDOM}", "T3=${3:$1_${2:$1}}", "T2=$2" ]
      }
    }
  • Open Neovim. Type each snippet prefix and press <C-j> (even if there is still active session). Explore from there.

Overview

Snippet is a template for a frequently used text. Typical workflow is to type snippet's (configurable) prefix and expand it into a snippet session: add some pre-defined text and allow user to interactively change/add at certain places.

This overview assumes default config for mappings and expand. See :h MiniSnippets.config and :h MiniSnippets-examples for more details.

Snippet structure

Snippet consists from three parts:

  • Prefix - identifier used to match against current text.
  • Body - actually inserted content with appropriate syntax.
  • Desc - description in human readable form.

Example: { prefix = 'tis', body = 'This is snippet', desc = 'Snip' } Typing tis and pressing "expand" mapping (<C-j> by default) will remove "tis", add "This is snippet", and place cursor at the end in Insert mode.

Syntax

Inserting just text after typing smaller prefix is already powerful enough. For more flexibility, snippet body can be formatted in a special way to provide extra features. This module implements support for syntax defined in LSP specification (with small deviations).

A quick overview of basic syntax features:

  • Tabstops are snippet parts meant for interactive editing at their location. They are denoted as $1, $2, etc.

    Navigating between them is called "jumping" and is done in numerical order of tabstop identifiers by pressing special keys: <C-l> and <C-h> to jump to next and previous tabstop respectively.

    Special tabstop $0 is called "final tabstop": it is used to decide when snippet session is automatically stopped and is visited last during jumping.

    Example: T1=$1 T2=$2 T0=$0 is expanded as T1= T2= T0= with three tabstops.

  • Tabstop can have placeholder: a text used if tabstop is not yet edited. Text is preserved if no editing is done. It follows this same syntax, which means it can itself contain tabstops with placeholders (i.e. be nested). Tabstop with placeholder is denoted as ${1:placeholder} ($1 is ${1:}).

    Example: T1=${1:text} T2=${2:<$1>} is expanded as T1=text T2=<text>; typing x at first placeholder results in T1=x T2=<x>; jumping once and typing y results in T1=x T2=y.

  • There can be several tabstops with same identifier. They are linked and updated in sync during text editing. Can also have different placeholders; they are forced to be the same as in the first (from left to right) tabstop.

    Example: T1=${1:text} T1=$1 is expanded as T1=text T1=text; typing x at first placeholder results in T1=x T1=x.

  • Tabstop can also have choices: suggestions about tabstop text. It is denoted as ${1|a,b,c|}. Choices are shown (with :h ins-completion like interface) after jumping to tabstop. First choice is used as placeholder.

    Example: T1=${1|left,right|} is expanded as T1=left.

  • Variables can be used to automatically insert text without user interaction. As tabstops, each one can have a placeholder which is used if variable is not defined. There is a special set of variables describing editor state.

    Example: V1=$TM_FILENAME V2=${NOTDEFINED:placeholder} is expanded as V1=current-file-basename V2=placeholder.

There are several differences LSP specification: not supporting variable transformations, wider set of supported special variables, and couple more. For more details see :h MiniSnippets-syntax-specification.

There is a :h MiniSnippets.parse() function for programmatically parsing snippet body into a comprehensible data structure.

Expand

Using snippets is done via what is called "expanding". It goes like this:

  • Type snippet prefix or its recognizable part.
  • Press <C-j> to expand. It will perform the following steps:
    • Prepare available snippets in current context (buffer + local language). This allows snippet setup to have general function loaders which return different snippets in different contexts.
    • Match text to the left of cursor with available prefixes. It first tries to do exact match and falls back to fuzzy matching.
    • If there are several matches, use vim.ui.select() to choose one.
    • Insert single matching snippet. If snippet contains tabstops, start snippet session.

For more details about each step see:

  • :h MiniSnippets.default_prepare()
  • :h MiniSnippets.default_match()
  • :h MiniSnippets.default_select()
  • :h MiniSnippets.default_insert()

Snippet session allows interactive editing at tabstop locations:

  • All tabstop locations are visualized depending on tabstop "state" (whether it is current/visited/unvisited/final and whether it was already edited).

    Empty tabstops are visualized with inline virtual text ( / for regular/final tabstops). It is removed after session is stopped.

  • Start session at first tabstop. Type text to replace placeholder. When finished with current tabstop, jump to next with <C-l>. Repeat. If changed mind about some previous tabstop, jump back with <C-h>. Jumping also wraps around the edge (first tabstop is next after final).

  • Starting another snippet session while there is an active one is allowed. This creates nested sessions: suspend current, start the new one. After newly created is stopped, resume the suspended one.

  • Stop session manually by pressing <C-c> or make it stop automatically: if final tabstop is current either make a text edit or exit to Normal mode. If snippet doesn't explicitly define final tabstop, it is added at the end of the snippet.

For more details about snippet session see :h MiniSnippets-session.

Management

Important: Out of the box 'mini.snippets' doesn't load any snippets, it should be done explicitly inside :h MiniSnippets.setup() following :h MiniSnippets.config.

The suggested approach to snippet management is to create dedicated files with snippet data and load them through function loaders in config.snippets. See Quickstart for basic (yet capable) snippet management config.

General idea of supported files is to have at least out of the box experience with common snippet collections. Namely rafamadriz/friendly-snippets.

The following files are supported:

  • Extensions:

    • Read/decoded as JSON object: *.json, *.code-snippets
    • Executed as Lua file and uses returned value: *.lua
  • Content:

    • Dict-like: object in JSON; returned table in Lua; no order guarantees.
    • Array-like: array in JSON; returned array table in Lua; preserves order.

Example of file content with a single snippet:

  • Lua dict-like: return { name = { prefix = 't', body = 'Text' } }
  • Lua array-like: return { { prefix = 't', body = 'Text', desc = 'name' } }
  • JSON dict-like: { "name": { "prefix": "t", "body": "Text" } }
  • JSON array-like: [ { "prefix": "t", "body": "Text", "desc": "name" } ]

General advice:

  • Put files in "snippets" subdirectory of any path in 'runtimepath' (like '$XDG_CONFIG_HOME/nvim/snippets/global.json'). This is compatible with :h MiniSnippets.gen_loader.from_runtime() and Quickstart.
  • Prefer *.json files with dict-like content if you want more cross platform setup. Otherwise use *.lua files with array-like content.

Notes:

  • There is no built-in support for VSCode-like "package.json" files. Define structure manually in :h MiniSnippets.setup() via built-in or custom loaders.
  • There is no built-in support for scope field of snippet data. Snippets are expected to be manually separated into smaller files and loaded on demand.

For supported snippet syntax see :h MiniSnippets-syntax-specification or Syntax.

Installation

This plugin can be installed as part of 'mini.nvim' library (recommended) or as a standalone Git repository.

During beta-testing phase there is only one branch to install from:

  • main (default, recommended) will have latest development version of plugin. All changes since last stable release should be perceived as being in beta testing phase (meaning they already passed alpha-testing and are moderately settled).

Here are code snippets for some common installation methods (use only one):

With mini.deps
Github repo Branch Code snippet
'mini.nvim' library Main Follow recommended 'mini.deps' installation
Standalone plugin Main add('echasnovski/mini.snippets')
With folke/lazy.nvim
Github repo Branch Code snippet
'mini.nvim' library Main { 'echasnovski/mini.nvim', version = false },
Standalone plugin Main { 'echasnovski/mini.snippets', version = false },
With junegunn/vim-plug
Github repo Branch Code snippet
'mini.nvim' library Main Plug 'echasnovski/mini.nvim'
Standalone plugin Main Plug 'echasnovski/mini.snippets'

Important: don't forget to call require('mini.snippets').setup() with non-empty snippets to have snippets to match from.

Note: if you are on Windows, there might be problems with too long file paths (like error: unable to create file <some file name>: Filename too long). Try doing one of the following:

  • Enable corresponding git global config value: git config --system core.longpaths true. Then try to reinstall.
  • Install plugin in other place with shorter path.

Default config

-- No need to copy this inside `setup()`. Will be used automatically.
{
  -- Array of snippets and loaders (see |MiniSnippets.config| for details).
  -- Nothing is defined by default. Add manually to have snippets to match.
  snippets = {},

  -- Module mappings. Use `''` (empty string) to disable one.
  mappings = {
    -- Expand snippet at cursor position. Created globally in Insert mode.
    expand = '<C-j>',

    -- Interact with default `expand.insert` session.
    -- Created for the duration of active session(s)
    jump_next = '<C-l>',
    jump_prev = '<C-h>',
    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,
  },
}

Similar plugins