8000 mini.nvim/lua/mini/snippets.lua at main · gerazov/mini.nvim · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
2683 lines (2435 loc) · 119 KB

snippets.lua

File metadata and controls

2683 lines (2435 loc) · 119 KB
--- *mini.snippets* Manage and expand snippets
--- *MiniSnippets*
---
--- MIT License Copyright (c) 2024 Evgeni Chasnovski
---
--- ==============================================================================
---
--- 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.
---
--- The template usually contains both pre-defined text and places (called
--- "tabstops") for user to interactively change/add text during snippet session.
---
--- This module supports (only) snippet syntax defined in LSP specification (with
--- small deviations). See |MiniSnippets-syntax-specification|.
---
--- Features:
--- - Manage snippet collection by adding it explicitly or with a flexible set of
--- performant built-in loaders. See |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 |MiniSnippets.default_prepare()|.
---
--- - Match which snippet to insert based on the currently typed text.
--- Supports both exact and fuzzy matching. See |MiniSnippets.default_match()|.
---
--- - Select from several matched snippets via `vim.ui.select()`.
--- See |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 preserving relative indent.
--- - 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 |MiniSnippets.default_insert()|.
---
--- - Exported function to parse snippet body into easy-to-reason data structure.
--- See |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:
--- - |MiniSnippets-glossary|
--- - |MiniSnippets-overview|
--- - |MiniSnippets-examples|
--- - |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'.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.snippets').setup({})` (replace `{}`
--- with your `config` table). It will create global Lua table `MiniSnippets` which
--- you can use for scripting or manually (with `:lua MiniSnippets.*`).
---
--- See |MiniSnippets.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minisnippets_config` which should have same structure as
--- `Minisnippets.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - 'L3MON4D3/LuaSnip':
--- - Both contain functionality to load snippets from file system.
--- This module provides several common loader generators while 'LuaSnip'
--- contains a more elaborate loading setup.
--- Also both require explicit opt-in for which snippets to load.
--- - Both support LSP snippet format. 'LuaSnip' also provides own more
--- elaborate snippet format which is out of scope for this module.
--- - 'LuaSnip' can autoexpand snippets, while this module always requires
--- an explicit user action to expand (by design).
--- - Both contain snippet expand functionality which differs in some aspects:
--- - 'LuaSnip' has an elaborate dynamic tabstop visualization config.
--- This module provides a handful of dedicated highlight groups.
--- - This module provides configurable visualization of empty tabstops.
--- - 'LusSnip' implements nested sessions by essentially merging them
--- into one. This module treats each nested session separately (to not
--- visually overload) while storing them in stack (first in last out).
--- - 'LuaSnip' uses |Select-mode| to power replacing current tabstop,
--- while this module always stays in |Insert-mode|. This enables easier
--- mapping understanding and more targeted highlighting.
--- - This module implements jumping which wraps after final tabstop
--- for more flexible navigation (enhanced with by a more flexible
--- autostopping rules), while 'LuaSnip' autostops session once
--- jumping reached the final tabstop.
---
--- - Built-in |vim.snippet| (on Neovim>=0.10):
--- - Does not contain functionality to load or match snippets (by design),
--- while this module does.
--- - Both contain expand functionality based on LSP snippet format.
--- Differences in how snippet sessions are handled are similar to
--- comparison with 'LuaSnip'.
---
--- - 'rafamadriz/friendly-snippets':
--- - A snippet collection plugin without features to manage or expand them.
--- This module is designed with 'friendly-snippets' compatibility in mind.
---
--- - 'abeldekat/cmp-mini-snippets':
--- - A source for 'hrsh7th/nvim-cmp' that integrates 'mini.snippets'.
---
--- # Highlight groups ~
---
--- * `MiniSnippetsCurrent` - current tabstop.
--- * `MiniSnippetsCurrentReplace` - current tabstop, placeholder is to be replaced.
--- * `MiniSnippetsFinal` - special `$0` tabstop.
--- * `MiniSnippetsUnvisited` - not yet visited tabstop(s).
--- * `MiniSnippetsVisited` - visited tabstop(s).
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable core functionality, set `vim.g.minisnippets_disable` (globally) or
--- `vim.b.minisnippets_disable` (for a buffer) to `true`. Considering high number
--- of different scenarios and customization intentions, writing exact rules
--- for disabling module's functionality is left to user. See
--- |mini.nvim-disabling-recipes| for common recipes.
--- `POSITION` Table representing position in a buffer. Fields:
--- - <line> `(number)` - line number (starts at 1).
--- - <col> `(number)` - column number (starts at 1).
---
--- `REGION` Table representing region in a buffer.
--- Fields: <from> and <to> for inclusive start/end POSITIONs.
---
--- `SNIPPET` Data about template to insert. Should contain fields:
--- - <prefix> - string snippet identifier.
--- - <body> - string snippet content with appropriate syntax.
--- - <desc> - string snippet description in human readable form.
--- Can also be used to mean snippet body if distinction is clear.
---
--- `SNIPPET SESSION` Interactive state for user to adjust inserted snippet.
---
--- `MATCHED SNIPPET` SNIPPET which contains <region> field with REGION that
--- matched it. Usually region needs to be removed.
---
--- `SNIPPET NODE` Unit of parsed SNIPPET body. See |MiniSnippets.parse()|.
---
--- `TABSTOP` Dedicated places in SNIPPET body for users to interactively
--- adjust. Specified in snippet body with `$` followed by digit(s).
---
--- `LINKED TABSTOPS` Different nodes assigned the same tabstop. Updated in sync.
---
--- `REFERENCE NODE` First (from left to right) node of linked tabstops.
--- Used to determine synced text and cursor placement after jump.
---
--- `EXPAND` Action to start snippet session based on currently typed text.
--- Always done in current buffer at cursor. Executed steps:
--- - `PREPARE` - resolve raw config snippets at context.
--- - `MATCH` - match resolved snippets at cursor position.
--- - `SELECT` - possibly choose among matched snippets.
--- - `INSERT` - insert selected snippet and start snippet session.
---@tag MiniSnippets-glossary
--- 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 |MiniSnippets.config| and |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.
---
--- *MiniSnippets-syntax-specification*
--- # 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). See this link for reference:
--- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#snippet_syntax
---
--- 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|}`. 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`.
---
--- What's different from LSP specification:
--- - Special set of variables is wider and is taken from VSCode specification:
--- https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables
--- Exceptions are `BLOCK_COMMENT_START` and `BLOCK_COMMENT_END` as Neovim doesn't
--- provide this information.
--- - Variable `TM_SELECTED_TEXT` is resolved as contents of |quote_quote| register.
--- It assumes that text is put there prior to expanding. For example, visually
--- select, press |c|, type prefix, and expand.
--- See |MiniSnippets-examples| for how to adjust this.
--- - Environment variables are recognized and supported: `V1=$VIMRUNTIME` will
--- use an actual value of |$VIMRUNTIME|.
--- - Variable transformations are not supported during snippet session. It would
--- require interacting with ECMAScript-like regular expressions for which there
--- is no easy way in Neovim. It may change in the future.
--- Transformations are recognized during parsing, though, with some exceptions:
--- - The `}` inside `if` of `${1:?if:else}` needs escaping (for technical reasons).
---
--- There is a |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:
--- - |MiniSnippets.default_prepare()|
--- - |MiniSnippets.default_match()|
--- - |MiniSnippets.default_select()|
--- - |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).
---
--- - If tabstop has choices, use <C-n> / <C-p> to select next / previous item.
---
--- - 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 |MiniSnippets-session|.
---
--- # Management ~
---
--- Out of the box 'mini.snippets' doesn't load any snippets, it should be done
--- explicitly inside |MiniSnippets.setup()| following |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 |MiniSnippets-examples| for basic (yet capable) snippet management config.
---
--- *MiniSnippets-file-specification*
--- 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 (|vim.json.decode()|): `*.json`, `*.code-snippets`
--- - Executed as Lua file (|dofile()|) 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 |MiniSnippets.gen_loader.from_runtime()| and
--- example from |MiniSnippets-examples|.
--- - Prefer `*.json` files with dict-like content if you want more cross platfrom
--- 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 |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 |MiniSnippets-syntax-specification|.
---
--- # Demo ~
---
--- The best way to grasp the design of snippet management and expansion is to
--- try them out yourself. Here are 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\n\tT1=$1" },
--- "Variables": { "prefix": "va", "body": "Runtime: $VIMRUNTIME\n" },
--- "Complex": {
--- "prefix": "co",
--- "body": [ "T1=${1:$RANDOM}", "T3=${3:$1_${2:$1}}", "T2=$2" ]
--- }
--- }
--- <
--- - Set up 'mini.snippets' as recommended in |MiniSnippets-examples|.
--- - Open Neovim. Type each snippet prefix and press <C-j> (even if there is
--- still active session). Explore from there.
---
---@tag MiniSnippets-overview
--- # Basic snippet management config ~
---
--- Example of snippet management setup that should cover most cases: >lua
---
--- -- Setup
--- local gen_loader = require('mini.snippets').gen_loader
--- require('mini.snippets').setup({
--- snippets = {
--- -- Load custom file with global snippets first
--- 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 |MiniSnippets.gen_loader.from_lang()|).
---
--- Create language snippets manually (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 and |MiniSnippets.read_file()| 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 |MiniSnippets.gen_loader| about how
--- to reset cache if necessary.
---
--- # Select from all available snippets in current context ~
---
--- With |MiniSnippets.default_match()|, expand snippets (<C-j> by default) at line
--- start or after whitespace. To be able to always select from all current
--- context snippets, make mapping similar to the following: >lua
---
--- local rhs = function() MiniSnippets.expand({ match = false }) end
--- vim.keymap.set('i', '<C-g><C-j>', rhs, { desc = 'Expand all' })
--- <
--- # "Supertab"-like <Tab> / <S-Tab> mappings ~
---
--- 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 <Tab> to "expand or jump" and <S-Tab> to "jump to previous": >lua
---
--- local snippets = require('mini.snippets')
--- local match_strict = function(snips)
--- -- Do not match with whitespace to cursor's left
--- return snippets.default_match(snips, { pattern_fuzzy = '%S+' })
--- end
--- snippets.setup({
--- -- ... Set up snippets ...
--- mappings = { expand = '', jump_next = '', jump_prev = '' },
--- expand = { match = match_strict },
--- })
--- local expand_or_jump = function()
--- local can_expand = #MiniSnippets.expand({ insert = false }) > 0
--- if can_expand then vim.schedule(MiniSnippets.expand); return '' end
--- local is_active = MiniSnippets.session.get() ~= nil
--- if is_active then MiniSnippets.session.jump('next'); return '' end
--- return '\t'
--- end
--- local jump_prev = function() MiniSnippets.session.jump('prev') end
--- vim.keymap.set('i', '<Tab>', expand_or_jump, { expr = true })
--- vim.keymap.set('i', '<S-Tab>', jump_prev)
--- <
--- # Stop session immediately after jumping to final tabstop ~
---
--- Utilize a dedicated |MiniSnippets-events|: >lua
---
--- local fin_stop = function(args)
--- if args.data.tabstop_to == '0' then MiniSnippets.session.stop() end
--- end
--- local au_opts = { pattern = 'MiniSnippetsSessionJump', callback = fin_stop }
--- vim.api.nvim_create_autocmd('User', au_opts)
--- <
--- # Stop all sessions on Normal mode exit ~
---
--- Use |ModeChanged| and |MiniSnippets-events| events: >lua
---
--- local make_stop = function()
--- local au_opts = { pattern = '*:n', }
--- au_opts.callback = function()
--- while MiniSnippets.session.get() do
--- MiniSnippets.session.stop()
--- end
--- end
--- vim.api.nvim_create_autocmd('ModeChanged', au_opts)
--- end
--- local opts = { pattern = 'MiniSnippetsSessionStart', callback = make_stop }
--- vim.api.nvim_create_autocmd('User', opts)
--- <
--- # Customize variable evaluation ~
---
--- Create environment variables and `config.expand.insert` wrapper: >lua
---
--- -- 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 = table.concat(vim.fn.getreg('a', true, true), '\n'),
--- }
--- return MiniSnippets.default_insert(snippet, { lookup = lookup })
--- end
---
--- require('mini.snippets').setup({
--- -- ... Set up snippets ...
--- expand = { insert = insert_with_lookup },
--- })
--- <
--- # Using Neovim's built-ins to insert snippet ~
---
--- Define custom `expand.insert` in |MiniSnippets.config| and mappings: >lua
---
--- require('mini.snippets').setup({
--- -- ... Set up snippets ...
--- expand = {
--- insert = function(snippet, _) vim.snippet.expand(snippet.body) end
--- }
--- })
--- -- Make jump mappings or skip to use built-in <Tab>/<S-Tab> in Neovim>=0.11
--- local jump_next = function()
--- if vim.snippet.active({direction = 1}) then return vim.snippet.jump(1) end
--- end
--- local jump_prev = function()
--- if vim.snippet.active({direction = -1}) then vim.snippet.jump(-1) end
--- end
--- vim.keymap.set({ 'i', 's' }, '<C-l>', jump_next)
--- vim.keymap.set({ 'i', 's' }, '<C-h>', jump_prev)
--- <
--- *MiniSnippets-in-other-plugins*
--- # Using 'mini.snippets' in other plugins ~
---
--- - Perform a `_G.MiniSnippets ~= nil` check before using any feature. This
--- ensures that user explici 10BC8 tly set up 'mini.snippets'.
---
--- - To insert snippet given its body (like |vim.snippet.expand()|), use: >lua
---
--- -- Use configured `insert` method with falling back to default
--- local insert = MiniSnippets.config.expand.insert
--- or MiniSnippets.default_insert
--- -- Insert at cursor
--- insert({ body = snippet })
--- <
--- - To get available snippets, use: >lua
---
--- -- Get snippets matched at cursor
--- MiniSnippets.expand({ insert = false })
---
--- -- Get all snippets available at cursor context
--- MiniSnippets.expand({ match = false, insert = false })
--- <
---@tag MiniSnippets-examples
---@alias __minisnippets_cache_opt <cache> `(boolean)` - whether to use cached output. Default: `true`.
---@alias __minisnippets_silent_opt <silent> `(boolean)` - whether to hide non-error messages. Default: `false`.
---@alias __minisnippets_loader_return function Snippet loader.
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
-- Module definition ==========================================================
local MiniSnippets = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniSnippets.config|.
---
---@usage >lua
--- require('mini.snippets').setup({}) -- replace {} with your config table
--- -- needs `snippets` field present
--- <
MiniSnippets.setup = function(config)
-- Export module
_G.MiniSnippets = MiniSnippets
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands()
-- Create default highlighting
H.create_default_hl()
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Loaded snippets ~
---
--- `config.snippets` is an array containing snippet data which can be: snippet
--- table, function loader, or (however deeply nested) array of snippet data.
---
--- Snippet is a table with the following fields:
---
--- - <prefix> `(string|table|nil)` - string used to match against current text.
--- If array, all strings should be used as separate prefixes.
--- - <body> `(string|table|nil)` - content of a snippet which should follow
--- the |MiniSnippets-syntax-specification|. Array is concatenated with "\n".
--- - <desc> `(string|table|nil)` - description of snippet. Can be used to display
--- snippets in a more human readable form. Array is concatenated with "\n".
---
--- Function loaders are expected to be called with single `context` table argument
--- (containing any data about current context) and return same as `config.snippets`
--- data structure.
---
--- `config.snippets` is resolved with `config.prepare` on every expand.
--- See |MiniSnippets.default_prepare()| for how it is done by default.
---
--- For a practical example see |MiniSnippets-examples|.
--- Here is an illustration of `config.snippets` customization capabilities: >lua
---
--- local gen_loader = require('mini.snippets').gen_loader
--- require('mini.snippets').setup({
--- snippets = {
--- -- Load custom file with global snippets first (order matters)
--- gen_loader.from_file('~/.config/nvim/snippets/global.json'),
---
--- -- Or add them here explicitly
--- { prefix='cdate', body='$CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE' },
---
--- -- Load snippets based on current language by reading files from
--- -- "snippets/" subdirectories from 'runtimepath' directories.
--- gen_loader.from_lang(),
---
--- -- Load project-local snippets with `gen_loader.from_file()`
--- -- and relative path (file doesn't have to be present)
--- gen_loader.from_file('.vscode/project.code-snippets'),
---
--- -- Custom loader for language-specific project-local snippets
--- function(context)
--- local rel_path = '.vscode/' .. context.lang .. '.code-snippets'
--- if vim.fn.filereadable(rel_path) == 0 then return end
--- return MiniSnippets.read_file(rel_path)
--- end,
---
--- -- Ensure that some prefixes are not used (as there is no `body`)
--- { prefix = { 'bad', 'prefix' } },
--- }
--- })
--- <
--- # Mappings ~
---
--- `config.mappings` describes which mappings are automatically created.
---
--- `mappings.expand` is created globally in Insert mode and is used to expand
--- snippet at cursor. Use |MiniSnippets.expand()| for custom mappings.
---
--- `mappings.jump_next`, `mappings.jump_prev`, and `mappings.stop` are created for
--- the duration of active snippet session(s) from |MiniSnippets.default_insert()|.
--- Used to jump to next/previous tabstop and stop active session respectively.
--- Use |MiniSnippets.session.jump()| and |MiniSnippets.session.stop()| for custom
--- Insert mode mappings.
--- Note: do not use `"<C-n>"` or `"<C-p>"` for any action as they conflict with
--- built-in completion: it forces them to mean "change focus to next/previous
--- completion item". This matters more frequently than when there is a tabstop
--- with choices due to how this module handles built-in completion during jumps.
---
--- # Expand ~
---
--- `config.expand` defines expand steps (see |MiniSnippets-glossary|), either after
--- pressing `mappings.expand` or starting manually via |MiniSnippets.expand()|.
---
--- `expand.prepare` is a function that takes `raw_snippets` in the form of
--- `config.snippets` and should return a plain array of snippets (as described
--- in |MiniSnippets-glossary|). Will be called on every |MiniSnippets.expand()| call.
--- If returns second value, it will be used as context for warning messages.
--- Default: |MiniSnippets.default_prepare()|.
---
--- `expand.match` is a function that takes `expand.prepare` output and returns
--- an array of matched snippets: one or several snippets user might intend to
--- eventually insert. Should sort matches in output from best to worst.
--- Entries can contain `region` field with current buffer region used to do
--- the match; usually it needs to be removed (similar to how |ins-completion|
--- and |abbreviations| work).
--- Default: |MiniSnippets.default_match()|
---
--- `expand.select` is a function that takes output of `expand.match` and function
--- that inserts snippet (and also ensures Insert mode and removes snippet's match
--- region). Should allow user to perform interactive snippet selection and
--- insert the chosen one. Designed to be compatible with |vim.ui.select()|.
--- Called for any non-empty `expand.match` output (even with single entry).
--- Default: |MiniSnippets.default_select()|
---
--- `expand.insert` is a function that takes single snippet table as input and
--- inserts snippet at cursor position. This is a main entry point for adding
--- text template to buffer and starting a snippet session.
--- If called inside |MiniSnippets.expand()| (which is a usual interactive case),
--- all it has to do is insert snippet at cursor position. Ensuring Insert mode
--- and removing matched snippet region is done beforehand.
--- Default: |MiniSnippets.default_insert()|
---
--- Illustration of `config.expand` customization: >lua
---
--- -- Supply extra data as context
--- local my_p = function(raw_snippets)
--- local _, cont = MiniSnippets.default_prepare({})
--- cont.cursor = vim.api.nvim_win_get_cursor()
--- return MiniSnippets.default_prepare(raw_snippets, { context = cont })
--- end
--- -- Perform fuzzy match based only on alphanumeric characters
--- local my_m = function(snippets)
--- return MiniSnippets.default_match(snippets, { pattern_fuzzy = '%w*' })
--- end
--- -- Always insert the best matched snippet
--- local my_s = function(snippets, insert) return insert(snippets[1]) end
--- -- Use different string to show empty tabstop as inline virtual text
--- local my_i = function(snippet)
--- return MiniSnippets.default_insert(snippet, { empty_tabstop = '$' })
--- end
---
--- require('mini.snippets').setup({
--- -- ... Set up snippets ...
--- expand = { prepare = my_p, match = my_m, select = my_s, insert = my_i }
--- })
--- <
MiniSnippets.config = {
-- 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,
},
}
--minidoc_afterlines_end
--- Expand snippet at cursor position
---
--- Perform expand steps (see |MiniSnippets-glossary|).
--- Initial raw snippets are taken from `config.snippets` in current buffer.
--- Snippets from `vim.b.minisnippets_config` are appended to global snippet array.
---
---@param opts table|nil Options. Same structure as `expand` in |MiniSnippets.config|
--- and uses its values as default. There are differences in allowed values:
--- - Use `match = false` to have all buffer snippets as matches.
--- - Use `select = false` to always expand the best match (if any).
--- - Use `insert = false` to return all matches without inserting.
---
--- Note: `opts.insert` is called after ensuring Insert mode, removing snippet's
--- match region, and positioning cursor.
---
---@return table|nil If `insert` is `false`, an array of matched snippets (`expand.match`
--- output). Otherwise `nil`.
---
---@usage >lua
--- -- Match, maybe select, and insert
--- MiniSnippets.expand()
---
--- -- Match and force expand the best match (if any)
--- MiniSnippets.expand({ select = false })
---
--- -- Use all current context snippets as matches
--- MiniSnippets.expand({ match = false })
---
--- -- Get all matched snippets
--- local matches = MiniSnippets.expand({ insert = false })
---
--- -- Get all current context snippets
--- local all = MiniSnippets.expand({ match = false, insert = false })
--- <
MiniSnippets.expand = function(opts)
if H.is_disabled() then return end
local config = H.get_config()
opts = vim.tbl_extend('force', config.expand, opts or {})
-- Validate
local prepare = opts.prepare or MiniSnippets.default_prepare
if not vim.is_callable(prepare) then H.error('`opts.prepare` should be callable') end
local match = false
if opts.match ~= false then match = opts.match or MiniSnippets.default_match end
if not (match == false or vim.is_callable(match)) then H.error('`opts.match` should be `false` or callable') end
local select = false
if opts.select ~= false then select = opts.select or MiniSnippets.default_select end
if not (select == false or vim.is_callable(select)) then H.error('`opts.select` should be `false` or callable') end
local insert = false
if opts.insert ~= false then insert = opts.insert or MiniSnippets.default_insert end
if not (insert == false or vim.is_callable(insert)) then H.error('`opts.insert` should be `false` or callable') end
-- Match
local all_snippets, context = prepare(config.snippets)
if not H.is_array_of(all_snippets, H.is_snippet) then H.error('`prepare` should return array of snippets') end
local matches = match == false and all_snippets or match(all_snippets)
if not H.is_array_of(matches, H.is_snippet) then H.error('`match` should return array of snippets') end
-- Act
if insert == false then return matches end
if #all_snippets == 0 then return H.notify('No snippets in context:\n' .. vim.inspect(context), 'WARN') end
if #matches == 0 then return H.notify('No matches in context:\n' .. vim.inspect(context), 'WARN') end
local insert_ext = H.make_extended_insert(insert)
if select == false then return insert_ext(matches[1]) end
select(matches, insert_ext)
end
--- Generate snippet loader
---
--- This is a table with function elements. Call to actually get a loader.
---
--- Common features for all produced loaders:
--- - Designed to work with |MiniSnippets-file-specification|.
--- - Cache output by default, i.e. second and later calls with same input value
--- don't read file system. Different loaders from same generator share cache.
--- Disable by setting `opts.cache` to `false`.
--- To clear all cache, call |MiniSnippets.setup()|. For example:
--- `MiniSnippets.setup(MiniSnippets.config)`
--- - Use |vim.notify()| to show problems during loading while trying to load as
--- much correctly defined snippet data as possible.
--- Disable by setting `opts.silent` to `true`.
MiniSnippets.gen_loader = {}
--- Generate language loader
---
--- Output loads files from "snippets/" subdirectories of 'runtimepath' matching
--- configured language patterns.
--- See |MiniSnippets.gen_loader.from_runtime()| for runtime loading details.
---
--- Language is taken from <lang> field (if present with string value) of `context`
--- argument used in loader calls during "prepare" stage.
--- This is compatible with |MiniSnippets.default_prepare()| and most snippet
--- collection plugins.
---
---@param opts table|nil Options. Possible values:
--- - <lang_patterns> `(table)` - map from language to array of runtime patterns
--- used to find snippet files, as in |MiniSnippets.gen_loader.from_runtime()|.
--- Patterns will be processed in order. With |MiniSnippets.default_prepare()|
--- it means if snippets have same prefix, data from later patterns is used.
---
--- Default pattern array (for non-empty language) is constructed as to read
--- `*.json` and `*.lua` files that are:
--- - Inside "snippets/" subdirectory named as language (files can be however
--- deeply nested).
--- - Named as language and is in "snippets/" directory (however deep).
--- Example for "lua" language: >lua
--- { 'lua/**/*.json', 'lua/**/*.lua', '**/lua.json', '**/lua.lua' }
--- <
--- Add entry for `""` (empty string) as language to be sourced when `lang`
--- context is empty string (which is usually temporary scratch buffers).
---
--- - __minisnippets_cache_opt
--- Note: caching is done per used runtime pattern, not `lang` value to allow
--- different `from_lang()` loaders to share cache.
--- - __minisnippets_silent_opt
---
---@return __minisnippets_loader_return
---
---@usage >lua
--- -- Adjust language patterns
--- local latex_patterns = { 'latex/**/*.json', '**/latex.json' }
--- local lang_patterns = { tex = latex_patterns, plaintex = latex_patterns }
--- local gen_loader = require('mini.snippets').gen_loader
--- require('mini.snippets').setup({
--- snippets = {
--- gen_loader.from_lang({ lang_patterns = lang_patterns }),
--- },
--- })
--- <
MiniSnippets.gen_loader.from_lang = function(opts)
opts = vim.tbl_extend('force', { lang_patterns = {}, cache = true, silent = false }, opts or {})
for lang, tbl in pairs(opts.lang_patterns) do
if type(lang) ~= 'string' then H.error('Keys of `opts.lang_patterns` should be string language names') end
if not H.is_array_of(tbl, H.is_string) then H.error('Values of `opts.lang_patterns` should be string arrays') end
end
local loaders, loader_opts = {}, { cache = opts.cache, silent = opts.silent }
return function(context)
local lang = (context or {}).lang
if type(lang) ~= 'string' then return {} end
local patterns = opts.lang_patterns[lang]
if patterns == nil and lang == '' then return {} end
-- NOTE: Don't use `{json,lua}` for better compatibility, as it seems that
-- its support might depend on the shell (and might not work on Windows).
-- Which is shame because fewer patterns used mean fewer calls to cache.
patterns = patterns
or { lang .. '/**/*.json', lang .. '/**/*.lua', '**/' .. lang .. '.json', '**/' .. lang .. '.lua' }
local res = {}
for _, pat in ipairs(patterns) do
local loader = loaders[pat] or MiniSnippets.gen_loader.from_runtime(pat, loader_opts)
loaders[pat] = loader
table.insert(res, loader(context))
end
return res
end
end
--- Generate runtime loader
---
--- Output loads files which match `pattern` inside "snippets/" directories from
--- 'runtimepath'. This is useful to simultaneously read several similarly
--- named files from different sources. Order from 'runtimepath' is preserved.
---
--- Typical case is loading snippets for a language from files like `xxx.{json,lua}`
--- but located in different "snippets/" directories inside 'runtimepath'.
--- - `<config>`/snippets/lua.json - manually curated snippets in user config.
--- - `<path/to/installed/plugin>`/snippets/lua.json - from installed plugin.
--- - `<config>`/after/snippets/lua.json - used to adjust snippets from plugins.
--- For example, remove some snippets by using prefixes and no body.
---
---@param pattern string Pattern of files to read. Can have wildcards as described
--- in |nvim_get_runtime_file()|. Example for "lua" language: `'lua.{json,lua}'`.
---@param opts table|nil Options. Possible fields:
--- - <all> `(boolean)` - whether to load from all matching runtime files.
--- Default: `true`.
--- - __minisnippets_cache_opt
--- Note: caching is done per `pattern` value, which assumes that both
--- 'runtimepath' value and snippet files do not change during Neovim session.
--- Caching this way gives significant speed improvement by reducing the need
--- to traverse file system on every snippet expand.
--- - __minisnippets_silent_opt
---
---@return __minisnippets_loader_return
MiniSnippets.gen_loader.from_runtime = function(pattern, opts)
if type(pattern) ~= 'string' then H.error('`pattern` should be string') end
opts = vim.tbl_extend('force', { all = true, cache = true, silent = false }, opts or {})
pattern = 'snippets/' .. pattern
local cache, read_opts = opts.cache, { cache = opts.cache, silent = opts.silent }
local read = function(p) return MiniSnippets.read_file(p, read_opts) end
return function()
if cache and H.cache.runtime[pattern] ~= nil then return vim.deepcopy(H.cache.runtime[pattern]) end
local res = vim.tbl_map(read, vim.api.nvim_get_runtime_file(pattern, opts.all))
if cache then H.cache.runtime[pattern] = vim.deepcopy(res) end
return res
end
end
--- Generate single file loader
---
--- Output is a thin wrapper around |MiniSnippets.read_file()| which will skip
--- warning if file is absent (other messages are still shown). Use it to load
--- file which is not guaranteed to exist (like project-local snippets).
---
---@param path string Same as in |MiniSnippets.read_file()|.
---@param opts table|nil Same as in |MiniSnippets.read_file()|.
---
---@return __minisnippets_loader_return
MiniSnippets.gen_loader.from_file = function(path, opts)
if type(path) ~= 'string' then H.error('`path` should be string') end
opts = vim.tbl_extend('force', { cache = true, silent = false }, opts or {})
return function()
local full_path = vim.fn.fnamemodify(path, ':p')
if vim.fn.filereadable(full_path) ~= 1 then return {} end
return MiniSnippets.read_file(full_path, opts) or {}
end
end
--- Read file with snippet data
---
---@param path string Path to file with snippets. Can be relative.
--- See |MiniSnippets-file-specification| for supported file formats.
---@param opts table|nil Options. Possible fields:
--- - __minisnippets_cache_opt
--- Note: Caching is done per full path only after successful reading.
--- - __minisnippets_silent_opt
---
---@return table|nil Array of snippets or `nil` if failed (also warn with |vim.notify()|
--- about the reason).
MiniSnippets.read_file = function(path, opts)
if type(path) ~= 'string' then H.error('`path` should be string') end
opts = vim.tbl_extend('force', { cache = true, silent = false }, opts or {})
path = vim.fn.fnamemodify(path, ':p')
local problem_prefix = 'There were problems reading file ' .. path .. ':\n'
if opts.cache and H.cache.file[path] ~= nil then return vim.deepcopy(H.cache.file[path]) end
if vim.fn.filereadable(path) ~= 1 then
return H.notify(problem_prefix .. 'File is absent or not readable', 'WARN', opts.silent)
end
local ext = path:match('%.([^%.]+)$')
if ext == nil or not (ext == 'lua' or ext == 'json' or ext == 'code-snippets') then
return H.notify(problem_prefix .. 'Extension is not supported', 'WARN', opts.silent)
end
local res = H.file_readers[ext](path, opts.silent)
-- Notify about problems but still cache if there are read snippets
local prob = table.concat(res.problems, '\n')
if prob ~= '' then H.notify(problem_prefix .. prob, 'WARN', opts.silent) end
if res.snippets == nil then return nil end
if opts.cache then H.cache.file[path] = vim.deepcopy(res.snippets) end
return res.snippets
end
--- Default prepare
---
--- Normalize raw snippets (as in `snippets` from |MiniSnippets.config|) based on
--- supplied context:
--- - Traverse and flatten nested arrays. Function loaders are executed with
--- `opts.context` as argument and output is processed recursively.
0