snippet.lua 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. local G = vim.lsp._snippet_grammar
  2. local snippet_group = vim.api.nvim_create_augroup('vim/snippet', {})
  3. local snippet_ns = vim.api.nvim_create_namespace('vim/snippet')
  4. local hl_group = 'SnippetTabstop'
  5. local jump_forward_key = '<tab>'
  6. local jump_backward_key = '<s-tab>'
  7. --- Returns the 0-based cursor position.
  8. ---
  9. --- @return integer, integer
  10. local function cursor_pos()
  11. local cursor = vim.api.nvim_win_get_cursor(0)
  12. return cursor[1] - 1, cursor[2]
  13. end
  14. --- Resolves variables (like `$name` or `${name:default}`) as follows:
  15. --- - When a variable is unknown (i.e.: its name is not recognized in any of the cases below), return `nil`.
  16. --- - When a variable isn't set, return its default (if any) or an empty string.
  17. ---
  18. --- Note that in some cases, the default is ignored since it's not clear how to distinguish an empty
  19. --- value from an unset value (e.g.: `TM_CURRENT_LINE`).
  20. ---
  21. --- @param var string
  22. --- @param default string
  23. --- @return string?
  24. local function resolve_variable(var, default)
  25. --- @param str string
  26. --- @return string
  27. local function expand_or_default(str)
  28. local expansion = vim.fn.expand(str) --[[@as string]]
  29. return expansion == '' and default or expansion
  30. end
  31. if var == 'TM_SELECTED_TEXT' then
  32. -- Snippets are expanded in insert mode only, so there's no selection.
  33. return default
  34. elseif var == 'TM_CURRENT_LINE' then
  35. return vim.api.nvim_get_current_line()
  36. elseif var == 'TM_CURRENT_WORD' then
  37. return expand_or_default('<cword>')
  38. elseif var == 'TM_LINE_INDEX' then
  39. return tostring(vim.fn.line('.') - 1)
  40. elseif var == 'TM_LINE_NUMBER' then
  41. return tostring(vim.fn.line('.'))
  42. elseif var == 'TM_FILENAME' then
  43. return expand_or_default('%:t')
  44. elseif var == 'TM_FILENAME_BASE' then
  45. return expand_or_default('%:t:r')
  46. elseif var == 'TM_DIRECTORY' then
  47. return expand_or_default('%:p:h:t')
  48. elseif var == 'TM_FILEPATH' then
  49. return expand_or_default('%:p')
  50. end
  51. -- Unknown variable.
  52. return nil
  53. end
  54. --- Transforms the given text into an array of lines (so no line contains `\n`).
  55. ---
  56. --- @param text string|string[]
  57. --- @return string[]
  58. local function text_to_lines(text)
  59. text = type(text) == 'string' and { text } or text
  60. --- @cast text string[]
  61. return vim.split(table.concat(text), '\n', { plain = true })
  62. end
  63. --- Computes the 0-based position of a tabstop located at the end of `snippet` and spanning
  64. --- `placeholder` (if given).
  65. ---
  66. --- @param snippet string[]
  67. --- @param placeholder string?
  68. --- @return Range4
  69. local function compute_tabstop_range(snippet, placeholder)
  70. local cursor_row, cursor_col = cursor_pos()
  71. local snippet_text = text_to_lines(snippet)
  72. local placeholder_text = text_to_lines(placeholder or '')
  73. local start_row = cursor_row + #snippet_text - 1
  74. local start_col = #(snippet_text[#snippet_text] or '')
  75. -- Add the cursor's column offset to the first line.
  76. if start_row == cursor_row then
  77. start_col = start_col + cursor_col
  78. end
  79. local end_row = start_row + #placeholder_text - 1
  80. local end_col = (start_row == end_row and start_col or 0)
  81. + #(placeholder_text[#placeholder_text] or '')
  82. return { start_row, start_col, end_row, end_col }
  83. end
  84. --- Returns the range spanned by the respective extmark.
  85. ---
  86. --- @param bufnr integer
  87. --- @param extmark_id integer
  88. --- @return Range4
  89. local function get_extmark_range(bufnr, extmark_id)
  90. local mark = vim.api.nvim_buf_get_extmark_by_id(bufnr, snippet_ns, extmark_id, { details = true })
  91. --- @diagnostic disable-next-line: undefined-field
  92. return { mark[1], mark[2], mark[3].end_row, mark[3].end_col }
  93. end
  94. --- @class (private) vim.snippet.Tabstop
  95. --- @field extmark_id integer
  96. --- @field bufnr integer
  97. --- @field index integer
  98. --- @field choices? string[]
  99. local Tabstop = {}
  100. --- Creates a new tabstop.
  101. ---
  102. --- @package
  103. --- @param index integer
  104. --- @param bufnr integer
  105. --- @param range Range4
  106. --- @param choices? string[]
  107. --- @return vim.snippet.Tabstop
  108. function Tabstop.new(index, bufnr, range, choices)
  109. local extmark_id = vim.api.nvim_buf_set_extmark(bufnr, snippet_ns, range[1], range[2], {
  110. right_gravity = true,
  111. end_right_gravity = true,
  112. end_line = range[3],
  113. end_col = range[4],
  114. hl_group = hl_group,
  115. })
  116. local self = setmetatable(
  117. { extmark_id = extmark_id, bufnr = bufnr, index = index, choices = choices },
  118. { __index = Tabstop }
  119. )
  120. return self
  121. end
  122. --- Returns the tabstop's range.
  123. ---
  124. --- @package
  125. --- @return Range4
  126. function Tabstop:get_range()
  127. return get_extmark_range(self.bufnr, self.extmark_id)
  128. end
  129. --- Returns the text spanned by the tabstop.
  130. ---
  131. --- @package
  132. --- @return string
  133. function Tabstop:get_text()
  134. local range = self:get_range()
  135. return table.concat(
  136. vim.api.nvim_buf_get_text(self.bufnr, range[1], range[2], range[3], range[4], {}),
  137. '\n'
  138. )
  139. end
  140. --- Sets the tabstop's text.
  141. ---
  142. --- @package
  143. --- @param text string
  144. function Tabstop:set_text(text)
  145. local range = self:get_range()
  146. vim.api.nvim_buf_set_text(self.bufnr, range[1], range[2], range[3], range[4], text_to_lines(text))
  147. end
  148. --- Sets the right gravity of the tabstop's extmark.
  149. ---
  150. --- @package
  151. --- @param right_gravity boolean
  152. function Tabstop:set_right_gravity(right_gravity)
  153. local range = self:get_range()
  154. self.extmark_id = vim.api.nvim_buf_set_extmark(self.bufnr, snippet_ns, range[1], range[2], {
  155. right_gravity = right_gravity,
  156. end_right_gravity = true,
  157. end_line = range[3],
  158. end_col = range[4],
  159. hl_group = hl_group,
  160. })
  161. end
  162. --- @class (private) vim.snippet.Session
  163. --- @field bufnr integer
  164. --- @field extmark_id integer
  165. --- @field tabstops table<integer, vim.snippet.Tabstop[]>
  166. --- @field current_tabstop vim.snippet.Tabstop
  167. --- @field tab_keymaps { i: table<string, any>?, s: table<string, any>? }
  168. --- @field shift_tab_keymaps { i: table<string, any>?, s: table<string, any>? }
  169. local Session = {}
  170. --- Creates a new snippet session in the current buffer.
  171. ---
  172. --- @package
  173. --- @param bufnr integer
  174. --- @param snippet_extmark integer
  175. --- @param tabstop_data table<integer, { range: Range4, choices?: string[] }[]>
  176. --- @return vim.snippet.Session
  177. function Session.new(bufnr, snippet_extmark, tabstop_data)
  178. local self = setmetatable({
  179. bufnr = bufnr,
  180. extmark_id = snippet_extmark,
  181. tabstops = {},
  182. current_tabstop = Tabstop.new(0, bufnr, { 0, 0, 0, 0 }),
  183. tab_keymaps = { i = nil, s = nil },
  184. shift_tab_keymaps = { i = nil, s = nil },
  185. }, { __index = Session })
  186. -- Create the tabstops.
  187. for index, ranges in pairs(tabstop_data) do
  188. for _, data in ipairs(ranges) do
  189. self.tabstops[index] = self.tabstops[index] or {}
  190. table.insert(self.tabstops[index], Tabstop.new(index, self.bufnr, data.range, data.choices))
  191. end
  192. end
  193. self:set_keymaps()
  194. return self
  195. end
  196. --- Sets the snippet navigation keymaps.
  197. ---
  198. --- @package
  199. function Session:set_keymaps()
  200. local function maparg(key, mode)
  201. local map = vim.fn.maparg(key, mode, false, true) --[[ @as table ]]
  202. if not vim.tbl_isempty(map) and map.buffer == 1 then
  203. return map
  204. else
  205. return nil
  206. end
  207. end
  208. local function set(jump_key, direction)
  209. vim.keymap.set({ 'i', 's' }, jump_key, function()
  210. return vim.snippet.active({ direction = direction })
  211. and '<cmd>lua vim.snippet.jump(' .. direction .. ')<cr>'
  212. or jump_key
  213. end, { expr = true, silent = true, buffer = self.bufnr })
  214. end
  215. self.tab_keymaps = {
  216. i = maparg(jump_forward_key, 'i'),
  217. s = maparg(jump_forward_key, 's'),
  218. }
  219. self.shift_tab_keymaps = {
  220. i = maparg(jump_backward_key, 'i'),
  221. s = maparg(jump_backward_key, 's'),
  222. }
  223. set(jump_forward_key, 1)
  224. set(jump_backward_key, -1)
  225. end
  226. --- Restores/deletes the keymaps used for snippet navigation.
  227. ---
  228. --- @package
  229. function Session:restore_keymaps()
  230. local function restore(keymap, lhs, mode)
  231. if keymap then
  232. vim._with({ buf = self.bufnr }, function()
  233. vim.fn.mapset(keymap)
  234. end)
  235. else
  236. vim.api.nvim_buf_del_keymap(self.bufnr, mode, lhs)
  237. end
  238. end
  239. restore(self.tab_keymaps.i, jump_forward_key, 'i')
  240. restore(self.tab_keymaps.s, jump_forward_key, 's')
  241. restore(self.shift_tab_keymaps.i, jump_backward_key, 'i')
  242. restore(self.shift_tab_keymaps.s, jump_backward_key, 's')
  243. end
  244. --- Returns the destination tabstop index when jumping in the given direction.
  245. ---
  246. --- @package
  247. --- @param direction vim.snippet.Direction
  248. --- @return integer?
  249. function Session:get_dest_index(direction)
  250. local tabstop_indexes = vim.tbl_keys(self.tabstops) --- @type integer[]
  251. table.sort(tabstop_indexes)
  252. for i, index in ipairs(tabstop_indexes) do
  253. if index == self.current_tabstop.index then
  254. local dest_index = tabstop_indexes[i + direction] --- @type integer?
  255. -- When jumping forwards, $0 is the last tabstop.
  256. if not dest_index and direction == 1 then
  257. dest_index = 0
  258. end
  259. -- When jumping backwards, make sure we don't think that $0 is the first tabstop.
  260. if dest_index == 0 and direction == -1 then
  261. dest_index = nil
  262. end
  263. return dest_index
  264. end
  265. end
  266. end
  267. --- Sets the right gravity of the tabstop group with the given index.
  268. ---
  269. --- @package
  270. --- @param index integer
  271. --- @param right_gravity boolean
  272. function Session:set_group_gravity(index, right_gravity)
  273. for _, tabstop in ipairs(self.tabstops[index]) do
  274. tabstop:set_right_gravity(right_gravity)
  275. end
  276. end
  277. local M = { session = nil }
  278. --- Displays the choices for the given tabstop as completion items.
  279. ---
  280. --- @param tabstop vim.snippet.Tabstop
  281. local function display_choices(tabstop)
  282. assert(tabstop.choices, 'Tabstop has no choices')
  283. local start_col = tabstop:get_range()[2] + 1
  284. local matches = {} --- @type table[]
  285. for _, choice in ipairs(tabstop.choices) do
  286. matches[#matches + 1] = { word = choice }
  287. end
  288. vim.defer_fn(function()
  289. vim.fn.complete(start_col, matches)
  290. end, 100)
  291. end
  292. --- Select the given tabstop range.
  293. ---
  294. --- @param tabstop vim.snippet.Tabstop
  295. local function select_tabstop(tabstop)
  296. --- @param keys string
  297. local function feedkeys(keys)
  298. keys = vim.api.nvim_replace_termcodes(keys, true, false, true)
  299. vim.api.nvim_feedkeys(keys, 'n', true)
  300. end
  301. --- NOTE: We don't use `vim.api.nvim_win_set_cursor` here because it causes the cursor to end
  302. --- at the end of the selection instead of the start.
  303. ---
  304. --- @param row integer
  305. --- @param col integer
  306. local function move_cursor_to(row, col)
  307. local line = vim.fn.getline(row) --[[ @as string ]]
  308. col = math.max(vim.fn.strchars(line:sub(1, col)) - 1, 0)
  309. feedkeys(string.format('%sG0%s', row, string.rep('<Right>', col)))
  310. end
  311. local range = tabstop:get_range()
  312. local mode = vim.fn.mode()
  313. if vim.fn.pumvisible() ~= 0 then
  314. -- Close the choice completion menu if open.
  315. vim.fn.complete(vim.fn.col('.'), {})
  316. end
  317. -- Move the cursor to the start of the tabstop.
  318. vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] })
  319. -- For empty, choice and the final tabstops, start insert mode at the end of the range.
  320. if tabstop.choices or tabstop.index == 0 or (range[1] == range[3] and range[2] == range[4]) then
  321. if mode ~= 'i' then
  322. if mode == 's' then
  323. feedkeys('<Esc>')
  324. end
  325. vim.cmd.startinsert({ bang = range[4] >= #vim.api.nvim_get_current_line() })
  326. end
  327. if tabstop.choices then
  328. display_choices(tabstop)
  329. end
  330. else
  331. -- Else, select the tabstop's text.
  332. if mode ~= 'n' then
  333. feedkeys('<Esc>')
  334. end
  335. move_cursor_to(range[1] + 1, range[2] + 1)
  336. feedkeys('v')
  337. move_cursor_to(range[3] + 1, range[4])
  338. feedkeys('o<c-g><c-r>_')
  339. end
  340. end
  341. --- Sets up the necessary autocommands for snippet expansion.
  342. ---
  343. --- @param bufnr integer
  344. local function setup_autocmds(bufnr)
  345. vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, {
  346. group = snippet_group,
  347. desc = 'Update snippet state when the cursor moves',
  348. buffer = bufnr,
  349. callback = function()
  350. -- Just update the tabstop in insert and select modes.
  351. if not vim.fn.mode():match('^[isS]') then
  352. return
  353. end
  354. local cursor_row, cursor_col = cursor_pos()
  355. -- The cursor left the snippet region.
  356. local snippet_range = get_extmark_range(bufnr, M._session.extmark_id)
  357. if
  358. cursor_row < snippet_range[1]
  359. or (cursor_row == snippet_range[1] and cursor_col < snippet_range[2])
  360. or cursor_row > snippet_range[3]
  361. or (cursor_row == snippet_range[3] and cursor_col > snippet_range[4])
  362. then
  363. M.stop()
  364. return true
  365. end
  366. for tabstop_index, tabstops in pairs(M._session.tabstops) do
  367. for _, tabstop in ipairs(tabstops) do
  368. local range = tabstop:get_range()
  369. if
  370. (cursor_row > range[1] or (cursor_row == range[1] and cursor_col >= range[2]))
  371. and (cursor_row < range[3] or (cursor_row == range[3] and cursor_col <= range[4]))
  372. then
  373. if tabstop_index ~= 0 then
  374. return
  375. end
  376. end
  377. end
  378. end
  379. -- The cursor is either not on a tabstop or we reached the end, so exit the session.
  380. M.stop()
  381. return true
  382. end,
  383. })
  384. vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, {
  385. group = snippet_group,
  386. desc = 'Update active tabstops when buffer text changes',
  387. buffer = bufnr,
  388. callback = function()
  389. -- Check that the snippet hasn't been deleted.
  390. local snippet_range = get_extmark_range(M._session.bufnr, M._session.extmark_id)
  391. if
  392. (snippet_range[1] == snippet_range[3] and snippet_range[2] == snippet_range[4])
  393. or snippet_range[3] + 1 > vim.fn.line('$')
  394. then
  395. M.stop()
  396. end
  397. if not M.active() then
  398. return true
  399. end
  400. -- Sync the tabstops in the current group.
  401. local current_tabstop = M._session.current_tabstop
  402. local current_text = current_tabstop:get_text()
  403. for _, tabstop in ipairs(M._session.tabstops[current_tabstop.index]) do
  404. if tabstop.extmark_id ~= current_tabstop.extmark_id then
  405. tabstop:set_text(current_text)
  406. end
  407. end
  408. end,
  409. })
  410. vim.api.nvim_create_autocmd('BufLeave', {
  411. group = snippet_group,
  412. desc = 'Stop the snippet session when leaving the buffer',
  413. buffer = bufnr,
  414. callback = function()
  415. M.stop()
  416. end,
  417. })
  418. end
  419. --- Expands the given snippet text.
  420. --- Refer to https://microsoft.github.io/language-server-protocol/specification/#snippet_syntax
  421. --- for the specification of valid input.
  422. ---
  423. --- Tabstops are highlighted with |hl-SnippetTabstop|.
  424. ---
  425. --- @param input string
  426. function M.expand(input)
  427. local snippet = G.parse(input)
  428. local snippet_text = {}
  429. local base_indent = vim.api.nvim_get_current_line():match('^%s*') or ''
  430. -- Get the placeholders we should use for each tabstop index.
  431. --- @type table<integer, string>
  432. local placeholders = {}
  433. for _, child in ipairs(snippet.data.children) do
  434. local type, data = child.type, child.data
  435. if type == G.NodeType.Placeholder then
  436. --- @cast data vim.snippet.PlaceholderData
  437. local tabstop, value = data.tabstop, tostring(data.value)
  438. if placeholders[tabstop] and placeholders[tabstop] ~= value then
  439. error('Snippet has multiple placeholders for tabstop $' .. tabstop)
  440. end
  441. placeholders[tabstop] = value
  442. end
  443. end
  444. -- Keep track of tabstop nodes during expansion.
  445. --- @type table<integer, { range: Range4, choices?: string[] }[]>
  446. local tabstop_data = {}
  447. --- @param index integer
  448. --- @param placeholder? string
  449. --- @param choices? string[]
  450. local function add_tabstop(index, placeholder, choices)
  451. tabstop_data[index] = tabstop_data[index] or {}
  452. local range = compute_tabstop_range(snippet_text, placeholder)
  453. table.insert(tabstop_data[index], { range = range, choices = choices })
  454. end
  455. --- Appends the given text to the snippet, taking care of indentation.
  456. ---
  457. --- @param text string|string[]
  458. local function append_to_snippet(text)
  459. local snippet_lines = text_to_lines(snippet_text)
  460. -- Get the base indentation based on the current line and the last line of the snippet.
  461. if #snippet_lines > 0 then
  462. base_indent = base_indent .. (snippet_lines[#snippet_lines]:match('(^%s+)%S') or '') --- @type string
  463. end
  464. local shiftwidth = vim.fn.shiftwidth()
  465. local curbuf = vim.api.nvim_get_current_buf()
  466. local expandtab = vim.bo[curbuf].expandtab
  467. local lines = {} --- @type string[]
  468. for i, line in ipairs(text_to_lines(text)) do
  469. -- Replace tabs by spaces.
  470. if expandtab then
  471. line = line:gsub('\t', (' '):rep(shiftwidth)) --- @type string
  472. end
  473. -- Add the base indentation.
  474. if i > 1 then
  475. line = base_indent .. line
  476. end
  477. lines[#lines + 1] = line
  478. end
  479. table.insert(snippet_text, table.concat(lines, '\n'))
  480. end
  481. for _, child in ipairs(snippet.data.children) do
  482. local type, data = child.type, child.data
  483. if type == G.NodeType.Tabstop then
  484. --- @cast data vim.snippet.TabstopData
  485. local placeholder = placeholders[data.tabstop]
  486. add_tabstop(data.tabstop, placeholder)
  487. if placeholder then
  488. append_to_snippet(placeholder)
  489. end
  490. elseif type == G.NodeType.Placeholder then
  491. --- @cast data vim.snippet.PlaceholderData
  492. local value = placeholders[data.tabstop]
  493. add_tabstop(data.tabstop, value)
  494. append_to_snippet(value)
  495. elseif type == G.NodeType.Choice then
  496. --- @cast data vim.snippet.ChoiceData
  497. add_tabstop(data.tabstop, nil, data.values)
  498. elseif type == G.NodeType.Variable then
  499. --- @cast data vim.snippet.VariableData
  500. -- Try to get the variable's value.
  501. local value = resolve_variable(data.name, data.default and tostring(data.default) or '')
  502. if not value then
  503. -- Unknown variable, make this a tabstop and use the variable name as a placeholder.
  504. value = data.name
  505. local tabstop_indexes = vim.tbl_keys(tabstop_data)
  506. local index = math.max(unpack((#tabstop_indexes == 0 and { 0 }) or tabstop_indexes)) + 1
  507. add_tabstop(index, value)
  508. end
  509. append_to_snippet(value)
  510. elseif type == G.NodeType.Text then
  511. --- @cast data vim.snippet.TextData
  512. append_to_snippet(data.text)
  513. end
  514. end
  515. -- $0, which defaults to the end of the snippet, defines the final cursor position.
  516. -- Make sure the snippet has exactly one of these.
  517. if vim.tbl_contains(vim.tbl_keys(tabstop_data), 0) then
  518. assert(#tabstop_data[0] == 1, 'Snippet has multiple $0 tabstops')
  519. else
  520. add_tabstop(0)
  521. end
  522. snippet_text = text_to_lines(snippet_text)
  523. -- Insert the snippet text.
  524. local bufnr = vim.api.nvim_get_current_buf()
  525. local cursor_row, cursor_col = cursor_pos()
  526. vim.api.nvim_buf_set_text(bufnr, cursor_row, cursor_col, cursor_row, cursor_col, snippet_text)
  527. -- Create the session.
  528. local snippet_extmark = vim.api.nvim_buf_set_extmark(bufnr, snippet_ns, cursor_row, cursor_col, {
  529. end_line = cursor_row + #snippet_text - 1,
  530. end_col = #snippet_text > 1 and #snippet_text[#snippet_text] or cursor_col + #snippet_text[1],
  531. right_gravity = false,
  532. end_right_gravity = true,
  533. })
  534. M._session = Session.new(bufnr, snippet_extmark, tabstop_data)
  535. -- Jump to the first tabstop.
  536. M.jump(1)
  537. end
  538. --- @alias vim.snippet.Direction -1 | 1
  539. --- Jumps to the next (or previous) placeholder in the current snippet, if possible.
  540. ---
  541. --- For example, map `<Tab>` to jump while a snippet is active:
  542. ---
  543. --- ```lua
  544. --- vim.keymap.set({ 'i', 's' }, '<Tab>', function()
  545. --- if vim.snippet.active({ direction = 1 }) then
  546. --- return '<Cmd>lua vim.snippet.jump(1)<CR>'
  547. --- else
  548. --- return '<Tab>'
  549. --- end
  550. --- end, { expr = true })
  551. --- ```
  552. ---
  553. --- @param direction (vim.snippet.Direction) Navigation direction. -1 for previous, 1 for next.
  554. function M.jump(direction)
  555. -- Get the tabstop index to jump to.
  556. local dest_index = M._session and M._session:get_dest_index(direction)
  557. if not dest_index then
  558. return
  559. end
  560. -- Find the tabstop with the lowest range.
  561. local tabstops = M._session.tabstops[dest_index]
  562. local dest = tabstops[1]
  563. for _, tabstop in ipairs(tabstops) do
  564. local dest_range, range = dest:get_range(), tabstop:get_range()
  565. if (range[1] < dest_range[1]) or (range[1] == dest_range[1] and range[2] < dest_range[2]) then
  566. dest = tabstop
  567. end
  568. end
  569. -- Clear the autocommands so that we can move the cursor freely while selecting the tabstop.
  570. vim.api.nvim_clear_autocmds({ group = snippet_group, buffer = M._session.bufnr })
  571. -- Deactivate expansion of the current tabstop.
  572. M._session:set_group_gravity(M._session.current_tabstop.index, true)
  573. M._session.current_tabstop = dest
  574. select_tabstop(dest)
  575. -- Activate expansion of the destination tabstop.
  576. M._session:set_group_gravity(dest.index, false)
  577. -- Restore the autocommands.
  578. setup_autocmds(M._session.bufnr)
  579. end
  580. --- @class vim.snippet.ActiveFilter
  581. --- @field direction vim.snippet.Direction Navigation direction. -1 for previous, 1 for next.
  582. --- Returns `true` if there's an active snippet in the current buffer,
  583. --- applying the given filter if provided.
  584. ---
  585. --- You can use this function to navigate a snippet as follows:
  586. ---
  587. --- ```lua
  588. --- vim.keymap.set({ 'i', 's' }, '<Tab>', function()
  589. --- if vim.snippet.active({ direction = 1 }) then
  590. --- return '<Cmd>lua vim.snippet.jump(1)<CR>'
  591. --- else
  592. --- return '<Tab>'
  593. --- end
  594. --- end, { expr = true })
  595. --- ```
  596. ---
  597. --- @param filter? vim.snippet.ActiveFilter Filter to constrain the search with:
  598. --- - `direction` (vim.snippet.Direction): Navigation direction. Will return `true` if the snippet
  599. --- can be jumped in the given direction.
  600. --- @return boolean
  601. function M.active(filter)
  602. local active = M._session ~= nil and M._session.bufnr == vim.api.nvim_get_current_buf()
  603. local in_direction = true
  604. if active and filter and filter.direction then
  605. in_direction = M._session:get_dest_index(filter.direction) ~= nil
  606. end
  607. return active and in_direction
  608. end
  609. --- Exits the current snippet.
  610. function M.stop()
  611. if not M.active() then
  612. return
  613. end
  614. M._session:restore_keymaps()
  615. vim.api.nvim_clear_autocmds({ group = snippet_group, buffer = M._session.bufnr })
  616. vim.api.nvim_buf_clear_namespace(M._session.bufnr, snippet_ns, 0, -1)
  617. M._session = nil
  618. end
  619. return M