mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	 ff777f9a85
			
		
	
	ff777f9a85
	
	
	
		
			
			Close #34938 Close #35030 Close #35233 Close #35259 Close #35290 Close #35433 Close #35541 Close #35766 Close #35792 Co-authored-by: huylg <45591413+huylg@users.noreply.github.com> Co-authored-by: Jason Del Ponte <961963+jasdel@users.noreply.github.com> Co-authored-by: sooriya <74165167+thuvasooriya@users.noreply.github.com> Co-authored-by: Andrew Braxton <andrewcbraxton@gmail.com> Co-authored-by: Enric Calabuig <enric.calabuig@gmail.com> Co-authored-by: Augusto César Dias <augusto.c.dias@gmail.com> Co-authored-by: David Sierra DiazGranados <davidsierradz@gmail.com> Co-authored-by: Stepan Nikitin <90522882+vectravox@users.noreply.github.com> Co-authored-by: Emilien Breton <bricktech2000@gmail.com>
		
			
				
	
	
		
			688 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			688 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| local G = vim.lsp._snippet_grammar
 | |
| local snippet_group = vim.api.nvim_create_augroup('nvim.snippet', {})
 | |
| local snippet_ns = vim.api.nvim_create_namespace('nvim.snippet')
 | |
| local hl_group = 'SnippetTabstop'
 | |
| local hl_group_active = 'SnippetTabstopActive'
 | |
| 
 | |
| --- Returns the 0-based cursor position.
 | |
| ---
 | |
| --- @return integer, integer
 | |
| local function cursor_pos()
 | |
|   local cursor = vim.api.nvim_win_get_cursor(0)
 | |
|   return cursor[1] - 1, cursor[2]
 | |
| end
 | |
| 
 | |
| --- Resolves variables (like `$name` or `${name:default}`) as follows:
 | |
| --- - When a variable is unknown (i.e.: its name is not recognized in any of the cases below), return `nil`.
 | |
| --- - When a variable isn't set, return its default (if any) or an empty string.
 | |
| ---
 | |
| --- Note that in some cases, the default is ignored since it's not clear how to distinguish an empty
 | |
| --- value from an unset value (e.g.: `TM_CURRENT_LINE`).
 | |
| ---
 | |
| --- @param var string
 | |
| --- @param default string
 | |
| --- @return string?
 | |
| local function resolve_variable(var, default)
 | |
|   --- @param str string
 | |
|   --- @return string
 | |
|   local function expand_or_default(str)
 | |
|     local expansion = vim.fn.expand(str) --[[@as string]]
 | |
|     return expansion == '' and default or expansion
 | |
|   end
 | |
| 
 | |
|   if var == 'TM_SELECTED_TEXT' then
 | |
|     -- Snippets are expanded in insert mode only, so there's no selection.
 | |
|     return default
 | |
|   elseif var == 'TM_CURRENT_LINE' then
 | |
|     return vim.api.nvim_get_current_line()
 | |
|   elseif var == 'TM_CURRENT_WORD' then
 | |
|     return expand_or_default('<cword>')
 | |
|   elseif var == 'TM_LINE_INDEX' then
 | |
|     return tostring(vim.fn.line('.') - 1)
 | |
|   elseif var == 'TM_LINE_NUMBER' then
 | |
|     return tostring(vim.fn.line('.'))
 | |
|   elseif var == 'TM_FILENAME' then
 | |
|     return expand_or_default('%:t')
 | |
|   elseif var == 'TM_FILENAME_BASE' then
 | |
|     return expand_or_default('%:t:r')
 | |
|   elseif var == 'TM_DIRECTORY' then
 | |
|     return expand_or_default('%:p:h:t')
 | |
|   elseif var == 'TM_FILEPATH' then
 | |
|     return expand_or_default('%:p')
 | |
|   end
 | |
| 
 | |
|   -- Unknown variable.
 | |
|   return nil
 | |
| end
 | |
| 
 | |
| --- Transforms the given text into an array of lines (so no line contains `\n`).
 | |
| ---
 | |
| --- @param text string|string[]
 | |
| --- @return string[]
 | |
| local function text_to_lines(text)
 | |
|   text = type(text) == 'string' and { text } or text
 | |
|   --- @cast text string[]
 | |
|   return vim.split(table.concat(text), '\n', { plain = true })
 | |
| end
 | |
| 
 | |
| --- Computes the 0-based position of a tabstop located at the end of `snippet` and spanning
 | |
| --- `placeholder` (if given).
 | |
| ---
 | |
| --- @param snippet string[]
 | |
| --- @param placeholder string?
 | |
| --- @return Range4
 | |
| local function compute_tabstop_range(snippet, placeholder)
 | |
|   local cursor_row, cursor_col = cursor_pos()
 | |
|   local snippet_text = text_to_lines(snippet)
 | |
|   local placeholder_text = text_to_lines(placeholder or '')
 | |
|   local start_row = cursor_row + #snippet_text - 1
 | |
|   local start_col = #(snippet_text[#snippet_text] or '')
 | |
| 
 | |
|   -- Add the cursor's column offset to the first line.
 | |
|   if start_row == cursor_row then
 | |
|     start_col = start_col + cursor_col
 | |
|   end
 | |
| 
 | |
|   local end_row = start_row + #placeholder_text - 1
 | |
|   local end_col = (start_row == end_row and start_col or 0)
 | |
|     + #(placeholder_text[#placeholder_text] or '')
 | |
| 
 | |
|   return { start_row, start_col, end_row, end_col }
 | |
| end
 | |
| 
 | |
| --- Returns the range spanned by the respective extmark.
 | |
| ---
 | |
| --- @param bufnr integer
 | |
| --- @param extmark_id integer
 | |
| --- @return Range4
 | |
| local function get_extmark_range(bufnr, extmark_id)
 | |
|   local mark = vim.api.nvim_buf_get_extmark_by_id(bufnr, snippet_ns, extmark_id, { details = true })
 | |
| 
 | |
|   --- @diagnostic disable-next-line: undefined-field
 | |
|   return { mark[1], mark[2], mark[3].end_row, mark[3].end_col }
 | |
| end
 | |
| 
 | |
| --- @class (private) vim.snippet.Tabstop
 | |
| --- @field extmark_id integer
 | |
| --- @field bufnr integer
 | |
| --- @field index integer
 | |
| --- @field placement integer
 | |
| --- @field choices? string[]
 | |
| local Tabstop = {}
 | |
| 
 | |
| --- Creates a new tabstop.
 | |
| ---
 | |
| --- @package
 | |
| --- @param index integer
 | |
| --- @param bufnr integer
 | |
| --- @param placement integer
 | |
| --- @param range Range4
 | |
| --- @param choices? string[]
 | |
| --- @return vim.snippet.Tabstop
 | |
| function Tabstop.new(index, bufnr, placement, range, choices)
 | |
|   local extmark_id = vim.api.nvim_buf_set_extmark(bufnr, snippet_ns, range[1], range[2], {
 | |
|     right_gravity = true,
 | |
|     end_right_gravity = false,
 | |
|     end_line = range[3],
 | |
|     end_col = range[4],
 | |
|     hl_group = index == 1 and hl_group_active or hl_group,
 | |
|   })
 | |
| 
 | |
|   local self = setmetatable({
 | |
|     extmark_id = extmark_id,
 | |
|     bufnr = bufnr,
 | |
|     index = index,
 | |
|     placement = placement,
 | |
|     choices = choices,
 | |
|   }, { __index = Tabstop })
 | |
| 
 | |
|   return self
 | |
| end
 | |
| 
 | |
| --- Returns the tabstop's range.
 | |
| ---
 | |
| --- @package
 | |
| --- @return Range4
 | |
| function Tabstop:get_range()
 | |
|   return get_extmark_range(self.bufnr, self.extmark_id)
 | |
| end
 | |
| 
 | |
| --- Returns the text spanned by the tabstop.
 | |
| ---
 | |
| --- @package
 | |
| --- @return string
 | |
| function Tabstop:get_text()
 | |
|   local range = self:get_range()
 | |
|   return table.concat(
 | |
|     vim.api.nvim_buf_get_text(self.bufnr, range[1], range[2], range[3], range[4], {}),
 | |
|     '\n'
 | |
|   )
 | |
| end
 | |
| 
 | |
| --- Sets the tabstop's text.
 | |
| ---
 | |
| --- @package
 | |
| --- @param text string
 | |
| function Tabstop:set_text(text)
 | |
|   local range = self:get_range()
 | |
|   vim.api.nvim_buf_set_text(self.bufnr, range[1], range[2], range[3], range[4], text_to_lines(text))
 | |
| end
 | |
| 
 | |
| ---@alias (private) vim.snippet.TabStopGravity
 | |
| --- | "expand" Expand the current tabstop on text insert
 | |
| --- | "lock" The tabstop should NOT move on text insert
 | |
| --- | "shift" The tabstop should move on text insert (default)
 | |
| 
 | |
| --- Sets the right gravity of the tabstop's extmark.
 | |
| --- Sets the active highlight group for current ("expand") tabstops
 | |
| ---
 | |
| ---@package
 | |
| ---@param target vim.snippet.TabStopGravity
 | |
| function Tabstop:set_gravity(target)
 | |
|   local hl = hl_group
 | |
|   local right_gravity = true
 | |
|   local end_right_gravity = true
 | |
| 
 | |
|   if target == 'expand' then
 | |
|     hl = hl_group_active
 | |
|     right_gravity = false
 | |
|     end_right_gravity = true
 | |
|   elseif target == 'lock' then
 | |
|     right_gravity = false
 | |
|     end_right_gravity = false
 | |
|   end
 | |
| 
 | |
|   local range = self:get_range()
 | |
|   vim.api.nvim_buf_del_extmark(self.bufnr, snippet_ns, self.extmark_id)
 | |
|   self.extmark_id = vim.api.nvim_buf_set_extmark(self.bufnr, snippet_ns, range[1], range[2], {
 | |
|     right_gravity = right_gravity,
 | |
|     end_right_gravity = end_right_gravity,
 | |
|     end_line = range[3],
 | |
|     end_col = range[4],
 | |
|     hl_group = hl,
 | |
|   })
 | |
| end
 | |
| 
 | |
| --- @class (private) vim.snippet.Session
 | |
| --- @field bufnr integer
 | |
| --- @field extmark_id integer
 | |
| --- @field tabstops table<integer, vim.snippet.Tabstop[]>
 | |
| --- @field tabstop_placements integer[]
 | |
| --- @field current_tabstop vim.snippet.Tabstop
 | |
| --- @field tab_keymaps { i: table<string, any>?, s: table<string, any>? }
 | |
| --- @field shift_tab_keymaps { i: table<string, any>?, s: table<string, any>? }
 | |
| local Session = {}
 | |
| 
 | |
| --- Creates a new snippet session in the current buffer.
 | |
| ---
 | |
| --- @package
 | |
| --- @param bufnr integer
 | |
| --- @param snippet_extmark integer
 | |
| --- @param tabstop_data table<integer, { placement: integer, range: Range4, choices?: string[] }[]>
 | |
| --- @return vim.snippet.Session
 | |
| function Session.new(bufnr, snippet_extmark, tabstop_data)
 | |
|   local self = setmetatable({
 | |
|     bufnr = bufnr,
 | |
|     extmark_id = snippet_extmark,
 | |
|     tabstops = {},
 | |
|     tabstop_placements = {},
 | |
|     current_tabstop = Tabstop.new(0, bufnr, 0, { 0, 0, 0, 0 }),
 | |
|     tab_keymaps = { i = nil, s = nil },
 | |
|     shift_tab_keymaps = { i = nil, s = nil },
 | |
|   }, { __index = Session })
 | |
| 
 | |
|   -- Create the tabstops.
 | |
|   for index, ranges in pairs(tabstop_data) do
 | |
|     for _, data in ipairs(ranges) do
 | |
|       self.tabstops[index] = self.tabstops[index] or {}
 | |
|       table.insert(
 | |
|         self.tabstops[index],
 | |
|         Tabstop.new(index, self.bufnr, data.placement, data.range, data.choices)
 | |
|       )
 | |
|       table.insert(self.tabstop_placements, data.placement)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   return self
 | |
| end
 | |
| 
 | |
| --- Returns the destination tabstop index when jumping in the given direction.
 | |
| ---
 | |
| --- @package
 | |
| --- @param direction vim.snippet.Direction
 | |
| --- @return integer?
 | |
| function Session:get_dest_index(direction)
 | |
|   local tabstop_indexes = vim.tbl_keys(self.tabstops) --- @type integer[]
 | |
|   table.sort(tabstop_indexes)
 | |
|   for i, index in ipairs(tabstop_indexes) do
 | |
|     if index == self.current_tabstop.index then
 | |
|       local dest_index = tabstop_indexes[i + direction] --- @type integer?
 | |
|       -- When jumping forwards, $0 is the last tabstop.
 | |
|       if not dest_index and direction == 1 then
 | |
|         dest_index = 0
 | |
|       end
 | |
|       -- When jumping backwards, make sure we don't think that $0 is the first tabstop.
 | |
|       if dest_index == 0 and direction == -1 then
 | |
|         dest_index = nil
 | |
|       end
 | |
|       return dest_index
 | |
|     end
 | |
|   end
 | |
| end
 | |
| 
 | |
| --- Sets the right gravity for all the tabstops.
 | |
| ---
 | |
| --- @package
 | |
| function Session:set_gravity()
 | |
|   local index = self.current_tabstop.index
 | |
|   local all_tabstop_placements = self.tabstop_placements
 | |
|   local dest_tabstop_placements = {}
 | |
| 
 | |
|   for _, tabstop in ipairs(self.tabstops[index]) do
 | |
|     tabstop:set_gravity('expand')
 | |
|     table.insert(dest_tabstop_placements, tabstop.placement)
 | |
|   end
 | |
| 
 | |
|   for i, tabstops in pairs(self.tabstops) do
 | |
|     if i ~= index then
 | |
|       for _, tabstop in ipairs(tabstops) do
 | |
|         local placement = tabstop.placement + 1
 | |
|         -- Check if there other tabstops directly adjacent
 | |
|         while
 | |
|           vim.list_contains(all_tabstop_placements, placement)
 | |
|           and not vim.list_contains(dest_tabstop_placements, placement)
 | |
|         do
 | |
|           placement = placement + 1
 | |
|         end
 | |
| 
 | |
|         if vim.list_contains(dest_tabstop_placements, placement) then
 | |
|           tabstop:set_gravity('lock')
 | |
|         else
 | |
|           tabstop:set_gravity('shift')
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 | |
| 
 | |
| local M = { session = nil }
 | |
| 
 | |
| --- Displays the choices for the given tabstop as completion items.
 | |
| ---
 | |
| --- @param tabstop vim.snippet.Tabstop
 | |
| local function display_choices(tabstop)
 | |
|   assert(tabstop.choices, 'Tabstop has no choices')
 | |
| 
 | |
|   local text = tabstop:get_text()
 | |
|   local found_text = false
 | |
| 
 | |
|   local start_col = tabstop:get_range()[2] + 1
 | |
|   local matches = {} --- @type table[]
 | |
|   for _, choice in ipairs(tabstop.choices) do
 | |
|     if choice ~= text then
 | |
|       matches[#matches + 1] = { word = choice }
 | |
|     else
 | |
|       found_text = true
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   if found_text then
 | |
|     table.insert(matches, 1, text)
 | |
|   end
 | |
| 
 | |
|   vim.defer_fn(function()
 | |
|     vim.fn.complete(start_col, matches)
 | |
|   end, 100)
 | |
| end
 | |
| 
 | |
| --- Select the given tabstop range.
 | |
| ---
 | |
| --- @param tabstop vim.snippet.Tabstop
 | |
| local function select_tabstop(tabstop)
 | |
|   --- @param keys string
 | |
|   local function feedkeys(keys)
 | |
|     keys = vim.api.nvim_replace_termcodes(keys, true, false, true)
 | |
|     vim.api.nvim_feedkeys(keys, 'n', true)
 | |
|   end
 | |
| 
 | |
|   local range = tabstop:get_range()
 | |
|   local mode = vim.fn.mode()
 | |
| 
 | |
|   if vim.fn.pumvisible() ~= 0 then
 | |
|     -- Close the choice completion menu if open.
 | |
|     vim.fn.complete(vim.fn.col('.'), {})
 | |
|   end
 | |
| 
 | |
|   -- Move the cursor to the start of the tabstop.
 | |
|   vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] })
 | |
| 
 | |
|   -- For empty, choice and the final tabstops, start insert mode at the end of the range.
 | |
|   if tabstop.choices or tabstop.index == 0 or (range[1] == range[3] and range[2] == range[4]) then
 | |
|     if mode ~= 'i' then
 | |
|       if mode == 's' then
 | |
|         feedkeys('<Esc>')
 | |
|       end
 | |
|       vim.cmd.startinsert({ bang = range[4] >= #vim.api.nvim_get_current_line() })
 | |
|     end
 | |
|     if tabstop.choices then
 | |
|       vim.fn.cursor(range[3] + 1, range[4] + 1)
 | |
|       display_choices(tabstop)
 | |
|     end
 | |
|   else
 | |
|     -- Else, select the tabstop's text.
 | |
|     -- Need this exact order so cannot mix regular API calls with feedkeys, which
 | |
|     -- are not executed immediately. Use <Cmd> to set the cursor position.
 | |
|     local keys = {
 | |
|       mode ~= 'n' and '<Esc>' or '',
 | |
|       ('<Cmd>call cursor(%s,%s)<CR>'):format(range[1] + 1, range[2] + 1),
 | |
|       'v',
 | |
|       ('<Cmd>call cursor(%s,%s)<CR>'):format(range[3] + 1, range[4]),
 | |
|       'o<c-g><c-r>_',
 | |
|     }
 | |
|     feedkeys(table.concat(keys))
 | |
|   end
 | |
| end
 | |
| 
 | |
| --- Sets up the necessary autocommands for snippet expansion.
 | |
| ---
 | |
| --- @param bufnr integer
 | |
| local function setup_autocmds(bufnr)
 | |
|   vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, {
 | |
|     group = snippet_group,
 | |
|     desc = 'Update snippet state when the cursor moves',
 | |
|     buffer = bufnr,
 | |
|     callback = function()
 | |
|       -- Just update the tabstop in insert and select modes.
 | |
|       if not vim.fn.mode():match('^[isS]') then
 | |
|         return
 | |
|       end
 | |
| 
 | |
|       local cursor_row, cursor_col = cursor_pos()
 | |
| 
 | |
|       -- The cursor left the snippet region.
 | |
|       local snippet_range = get_extmark_range(bufnr, M._session.extmark_id)
 | |
|       if
 | |
|         cursor_row < snippet_range[1]
 | |
|         or (cursor_row == snippet_range[1] and cursor_col < snippet_range[2])
 | |
|         or cursor_row > snippet_range[3]
 | |
|         or (cursor_row == snippet_range[3] and cursor_col > snippet_range[4])
 | |
|       then
 | |
|         M.stop()
 | |
|         return true
 | |
|       end
 | |
| 
 | |
|       for tabstop_index, tabstops in pairs(M._session.tabstops) do
 | |
|         for _, tabstop in ipairs(tabstops) do
 | |
|           local range = tabstop:get_range()
 | |
|           if
 | |
|             (cursor_row > range[1] or (cursor_row == range[1] and cursor_col >= range[2]))
 | |
|             and (cursor_row < range[3] or (cursor_row == range[3] and cursor_col <= range[4]))
 | |
|           then
 | |
|             if tabstop_index ~= 0 then
 | |
|               return
 | |
|             end
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       -- The cursor is either not on a tabstop or we reached the end, so exit the session.
 | |
|       M.stop()
 | |
|       return true
 | |
|     end,
 | |
|   })
 | |
| 
 | |
|   vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI', 'TextChangedP' }, {
 | |
|     group = snippet_group,
 | |
|     desc = 'Update active tabstops when buffer text changes',
 | |
|     buffer = bufnr,
 | |
|     callback = function()
 | |
|       -- Check that the snippet hasn't been deleted.
 | |
|       local snippet_range = get_extmark_range(M._session.bufnr, M._session.extmark_id)
 | |
|       if
 | |
|         (snippet_range[1] == snippet_range[3] and snippet_range[2] == snippet_range[4])
 | |
|         or snippet_range[3] + 1 > vim.fn.line('$')
 | |
|       then
 | |
|         M.stop()
 | |
|       end
 | |
| 
 | |
|       if not M.active() then
 | |
|         return true
 | |
|       end
 | |
| 
 | |
|       -- Sync the tabstops in the current group.
 | |
|       local current_tabstop = M._session.current_tabstop
 | |
|       local current_text = current_tabstop:get_text()
 | |
|       for _, tabstop in ipairs(M._session.tabstops[current_tabstop.index]) do
 | |
|         if tabstop.extmark_id ~= current_tabstop.extmark_id then
 | |
|           tabstop:set_text(current_text)
 | |
|         end
 | |
|       end
 | |
|     end,
 | |
|   })
 | |
| 
 | |
|   vim.api.nvim_create_autocmd('BufLeave', {
 | |
|     group = snippet_group,
 | |
|     desc = 'Stop the snippet session when leaving the buffer',
 | |
|     buffer = bufnr,
 | |
|     callback = function()
 | |
|       M.stop()
 | |
|     end,
 | |
|   })
 | |
| end
 | |
| 
 | |
| --- Expands the given snippet text.
 | |
| --- Refer to https://microsoft.github.io/language-server-protocol/specification/#snippet_syntax
 | |
| --- for the specification of valid input.
 | |
| ---
 | |
| --- Tabstops are highlighted with |hl-SnippetTabstop| and |hl-SnippetTabstopActive|.
 | |
| ---
 | |
| --- @param input string
 | |
| function M.expand(input)
 | |
|   local snippet = G.parse(input)
 | |
|   local snippet_text = {}
 | |
|   ---@type string
 | |
|   local base_indent = vim.api.nvim_get_current_line():match('^%s*') or ''
 | |
| 
 | |
|   -- Get the placeholders we should use for each tabstop index.
 | |
|   --- @type table<integer, string>
 | |
|   local placeholders = {}
 | |
|   for _, child in ipairs(snippet.data.children) do
 | |
|     local type, data = child.type, child.data
 | |
|     if type == G.NodeType.Placeholder then
 | |
|       --- @cast data vim.snippet.PlaceholderData
 | |
|       local tabstop, value = data.tabstop, tostring(data.value)
 | |
|       if placeholders[tabstop] and placeholders[tabstop] ~= value then
 | |
|         error('Snippet has multiple placeholders for tabstop $' .. tabstop)
 | |
|       end
 | |
|       placeholders[tabstop] = value
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   -- Keep track of tabstop nodes during expansion.
 | |
|   --- @type table<integer, { placement: integer, range: Range4, choices?: string[] }[]>
 | |
|   local tabstop_data = {}
 | |
| 
 | |
|   --- @param placement integer
 | |
|   --- @param index integer
 | |
|   --- @param placeholder? string
 | |
|   --- @param choices? string[]
 | |
|   local function add_tabstop(placement, index, placeholder, choices)
 | |
|     tabstop_data[index] = tabstop_data[index] or {}
 | |
|     local range = compute_tabstop_range(snippet_text, placeholder)
 | |
|     table.insert(tabstop_data[index], { placement = placement, range = range, choices = choices })
 | |
|   end
 | |
| 
 | |
|   --- Appends the given text to the snippet, taking care of indentation.
 | |
|   ---
 | |
|   --- @param text string|string[]
 | |
|   local function append_to_snippet(text)
 | |
|     local shiftwidth = vim.fn.shiftwidth()
 | |
|     local curbuf = vim.api.nvim_get_current_buf()
 | |
|     local expandtab = vim.bo[curbuf].expandtab
 | |
| 
 | |
|     local lines = {} --- @type string[]
 | |
|     for i, line in ipairs(text_to_lines(text)) do
 | |
|       -- Replace tabs by spaces.
 | |
|       if expandtab then
 | |
|         line = line:gsub('\t', (' '):rep(shiftwidth)) --- @type string
 | |
|       end
 | |
|       -- Add the base indentation.
 | |
|       if i > 1 then
 | |
|         line = base_indent .. line
 | |
|       end
 | |
|       lines[#lines + 1] = line
 | |
|     end
 | |
| 
 | |
|     table.insert(snippet_text, table.concat(lines, '\n'))
 | |
|   end
 | |
| 
 | |
|   for index, child in ipairs(snippet.data.children) do
 | |
|     local type, data = child.type, child.data
 | |
|     if type == G.NodeType.Tabstop then
 | |
|       --- @cast data vim.snippet.TabstopData
 | |
|       local placeholder = placeholders[data.tabstop]
 | |
|       add_tabstop(index, data.tabstop, placeholder)
 | |
|       if placeholder then
 | |
|         append_to_snippet(placeholder)
 | |
|       end
 | |
|     elseif type == G.NodeType.Placeholder then
 | |
|       --- @cast data vim.snippet.PlaceholderData
 | |
|       local value = placeholders[data.tabstop]
 | |
|       add_tabstop(index, data.tabstop, value)
 | |
|       append_to_snippet(value)
 | |
|     elseif type == G.NodeType.Choice then
 | |
|       --- @cast data vim.snippet.ChoiceData
 | |
|       add_tabstop(index, data.tabstop, nil, data.values)
 | |
|     elseif type == G.NodeType.Variable then
 | |
|       --- @cast data vim.snippet.VariableData
 | |
|       -- Try to get the variable's value.
 | |
|       local value = resolve_variable(data.name, data.default and tostring(data.default) or '')
 | |
|       if not value then
 | |
|         -- Unknown variable, make this a tabstop and use the variable name as a placeholder.
 | |
|         value = data.name
 | |
|         local tabstop_indexes = vim.tbl_keys(tabstop_data)
 | |
|         local tabstop_index = math.max(unpack((#tabstop_indexes == 0 and { 0 }) or tabstop_indexes))
 | |
|           + 1
 | |
|         add_tabstop(index, tabstop_index, value)
 | |
|       end
 | |
|       append_to_snippet(value)
 | |
|     elseif type == G.NodeType.Text then
 | |
|       --- @cast data vim.snippet.TextData
 | |
|       append_to_snippet(data.text)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   -- $0, which defaults to the end of the snippet, defines the final cursor position.
 | |
|   -- Make sure the snippet has exactly one of these.
 | |
|   if vim.tbl_contains(vim.tbl_keys(tabstop_data), 0) then
 | |
|     assert(#tabstop_data[0] == 1, 'Snippet has multiple $0 tabstops')
 | |
|   else
 | |
|     add_tabstop(#snippet.data.children + 1, 0)
 | |
|   end
 | |
| 
 | |
|   snippet_text = text_to_lines(snippet_text)
 | |
| 
 | |
|   -- Insert the snippet text.
 | |
|   local bufnr = vim.api.nvim_get_current_buf()
 | |
|   local cursor_row, cursor_col = cursor_pos()
 | |
|   vim.api.nvim_buf_set_text(bufnr, cursor_row, cursor_col, cursor_row, cursor_col, snippet_text)
 | |
| 
 | |
|   -- Create the session.
 | |
|   local snippet_extmark = vim.api.nvim_buf_set_extmark(bufnr, snippet_ns, cursor_row, cursor_col, {
 | |
|     end_line = cursor_row + #snippet_text - 1,
 | |
|     end_col = #snippet_text > 1 and #snippet_text[#snippet_text] or cursor_col + #snippet_text[1],
 | |
|     right_gravity = false,
 | |
|     end_right_gravity = true,
 | |
|   })
 | |
|   M._session = Session.new(bufnr, snippet_extmark, tabstop_data)
 | |
| 
 | |
|   -- Jump to the first tabstop.
 | |
|   M.jump(1)
 | |
| end
 | |
| 
 | |
| --- @alias vim.snippet.Direction -1 | 1
 | |
| 
 | |
| --- Jumps to the next (or previous) placeholder in the current snippet, if possible.
 | |
| ---
 | |
| --- By default `<Tab>` is setup to jump if a snippet is active. The default mapping looks like:
 | |
| ---
 | |
| --- ```lua
 | |
| --- vim.keymap.set({ 'i', 's' }, '<Tab>', function()
 | |
| ---    if vim.snippet.active({ direction = 1 }) then
 | |
| ---      return '<Cmd>lua vim.snippet.jump(1)<CR>'
 | |
| ---    else
 | |
| ---      return '<Tab>'
 | |
| ---    end
 | |
| ---  end, { desc = '...', expr = true, silent = true })
 | |
| --- ```
 | |
| ---
 | |
| --- @param direction (vim.snippet.Direction) Navigation direction. -1 for previous, 1 for next.
 | |
| function M.jump(direction)
 | |
|   -- Get the tabstop index to jump to.
 | |
|   local dest_index = M._session and M._session:get_dest_index(direction)
 | |
|   if not dest_index then
 | |
|     return
 | |
|   end
 | |
| 
 | |
|   -- Find the tabstop with the lowest range.
 | |
|   local tabstops = M._session.tabstops[dest_index]
 | |
|   local dest = tabstops[1]
 | |
|   for _, tabstop in ipairs(tabstops) do
 | |
|     local dest_range, range = dest:get_range(), tabstop:get_range()
 | |
|     if (range[1] < dest_range[1]) or (range[1] == dest_range[1] and range[2] < dest_range[2]) then
 | |
|       dest = tabstop
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   -- Clear the autocommands so that we can move the cursor freely while selecting the tabstop.
 | |
|   vim.api.nvim_clear_autocmds({ group = snippet_group, buffer = M._session.bufnr })
 | |
| 
 | |
|   M._session.current_tabstop = dest
 | |
|   M._session:set_gravity()
 | |
|   select_tabstop(dest)
 | |
| 
 | |
|   -- The cursor is not on a tabstop so exit the session.
 | |
|   if dest.index == 0 then
 | |
|     M.stop()
 | |
|     return
 | |
|   end
 | |
| 
 | |
|   -- Restore the autocommands.
 | |
|   setup_autocmds(M._session.bufnr)
 | |
| end
 | |
| 
 | |
| --- @class vim.snippet.ActiveFilter
 | |
| --- @field direction vim.snippet.Direction Navigation direction. -1 for previous, 1 for next.
 | |
| 
 | |
| --- Returns `true` if there's an active snippet in the current buffer,
 | |
| --- applying the given filter if provided.
 | |
| ---
 | |
| --- @param filter? vim.snippet.ActiveFilter Filter to constrain the search with:
 | |
| --- - `direction` (vim.snippet.Direction): Navigation direction. Will return `true` if the snippet
 | |
| --- can be jumped in the given direction.
 | |
| --- @return boolean
 | |
| function M.active(filter)
 | |
|   local active = M._session ~= nil and M._session.bufnr == vim.api.nvim_get_current_buf()
 | |
| 
 | |
|   local in_direction = true
 | |
|   if active and filter and filter.direction then
 | |
|     in_direction = M._session:get_dest_index(filter.direction) ~= nil
 | |
|   end
 | |
| 
 | |
|   return active and in_direction
 | |
| end
 | |
| 
 | |
| --- Exits the current snippet.
 | |
| function M.stop()
 | |
|   if not M.active() then
 | |
|     return
 | |
|   end
 | |
| 
 | |
|   vim.api.nvim_clear_autocmds({ group = snippet_group, buffer = M._session.bufnr })
 | |
|   vim.api.nvim_buf_clear_namespace(M._session.bufnr, snippet_ns, 0, -1)
 | |
| 
 | |
|   M._session = nil
 | |
| end
 | |
| 
 | |
| return M
 |