Skip to content

Commit f9684e2

Browse files
committed
fix(snippets): filter out incompatible builtin snippets
Skip early incompatible builtin snippets using LSP grammar.
1 parent 230ccf8 commit f9684e2

File tree

2 files changed

+49
-23
lines changed

2 files changed

+49
-23
lines changed

lua/blink/cmp/sources/snippets/default/registry.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ local registry = {
1111
builtin_vars = require('blink.cmp.sources.snippets.default.builtin'),
1212
}
1313

14+
local user_config = vim.fn.stdpath('config')
1415
local utils = require('blink.cmp.sources.snippets.utils')
1516
local default_config = {
1617
friendly_snippets = true,
@@ -62,9 +63,10 @@ function registry:get_snippets_for_ft(filetype)
6263
for _, f in ipairs(files) do
6364
local contents = utils.read_file(f)
6465
if contents then
66+
local is_user_snippet = vim.startswith(f, user_config)
6567
local snippets = utils.parse_json_with_error_msg(f, contents)
6668
for _, key in ipairs(vim.tbl_keys(snippets)) do
67-
local snippet = utils.read_snippet(snippets[key], key)
69+
local snippet = utils.read_snippet(snippets[key], key, filetype, is_user_snippet)
6870
for _, snippet_def in pairs(snippet) do
6971
table.insert(loaded_snippets, snippet_def)
7072
end

lua/blink/cmp/sources/snippets/utils.lua

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local lsp_snippet_grammar = require('vim.lsp._snippet_grammar')
12
local utils = {
23
parse_cache = {},
34
}
@@ -31,20 +32,21 @@ end
3132
function utils.safe_parse(input)
3233
if utils.parse_cache[input] then return utils.parse_cache[input] end
3334

34-
local safe, parsed = pcall(vim.lsp._snippet_grammar.parse, input)
35+
local safe, parsed = pcall(lsp_snippet_grammar.parse, input)
3536
if not safe then return nil end
3637

3738
utils.parse_cache[input] = parsed
3839
return parsed
3940
end
4041

41-
---@type fun(snippet: blink.cmp.Snippet, fallback: string): table
42-
function utils.read_snippet(snippet, fallback)
43-
local snippets = {}
42+
---@type fun(snippet: blink.cmp.Snippet, fallback: string, filetype: string, is_user_snippet: boolean): table
43+
function utils.read_snippet(snippet, fallback, filetype, is_user_snippet)
4444
local prefix = snippet.prefix or fallback
45-
local description = snippet.description or fallback
46-
local body = snippet.body
45+
local body = utils.validate_body(snippet.body, prefix, filetype, is_user_snippet)
46+
if body == nil then return {} end
4747

48+
local snippets = {}
49+
local description = snippet.description or fallback
4850
if type(description) == 'table' then description = vim.fn.join(description, '') end
4951

5052
if type(prefix) == 'table' then
@@ -90,25 +92,47 @@ function utils.add_current_line_indentation(text)
9092
return table.concat(lines, '\n')
9193
end
9294

93-
function utils.get_tab_stops(snippet)
94-
local expanded_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(snippet)
95-
if not expanded_snippet then return end
96-
97-
local tabstops = {}
98-
local grammar = require('vim.lsp._snippet_grammar')
99-
local line = 1
100-
local character = 1
101-
for _, child in ipairs(expanded_snippet.data.children) do
102-
local lines = tostring(child) == '' and {} or vim.split(tostring(child), '\n')
103-
line = line + math.max(#lines - 1, 0)
104-
character = #lines == 0 and character or #lines > 1 and #lines[#lines] or (character + #lines[#lines])
105-
if child.type == grammar.NodeType.Placeholder or child.type == grammar.NodeType.Tabstop then
106-
table.insert(tabstops, { index = child.data.tabstop, line = line, character = character })
95+
---@param body string|string[]
96+
---@param prefix string
97+
---@param filetype string
98+
---@param is_user_snippet boolean
99+
---@return string|string[]|nil
100+
function utils.validate_body(body, prefix, filetype, is_user_snippet)
101+
if type(body) == 'table' then body = table.concat(body, '\n') end
102+
103+
-- Fix snippet from friendly snippets source, whenever possible
104+
-- stylua: ignore
105+
if not is_user_snippet then
106+
body = body
107+
:gsub(':\\${', ':${') -- unescape :${
108+
:gsub(':${(%w)\\}', ':${%1}') -- unescape :${..\\}
109+
:gsub('\\}}', '}}') -- unescape }}
110+
:gsub('\\([%(%))])', '%1') -- unescape parentheses
111+
:gsub(' \\([%(%))])\\', ' %1\\') -- unescape parens before backslash
112+
:gsub('([%s{%(%[])%$%${', '%1\\$${') -- escape $$ after whitespace/brackets
113+
:gsub('$: ', '\\$: ') -- escape $ before colon-space
114+
:gsub('(".*%w)%$(")', '%1\\$%2') -- escape dollar sign, e.g. "foo$" -> "foo\$"
115+
:gsub('$\\{', '\\${') -- wrong backslash position
116+
:gsub('(\\?)%$%W*(%$[%w{]+)%W*%$', function(e, a) return (e == '\\' and e or '\\') .. '$' .. a end)
117+
:gsub('(%${%d+|)([^}]+)(|})', function(s, o, e) return s .. o:gsub('\\', '\\\\') .. e end) -- Escape \ in options, e.g. \Huge -> \\Huge
118+
119+
if filetype == 'terraform' then
120+
body = body
121+
:gsub('= "\\${', '= "${')
122+
:gsub('= %["\\${', '= ["${')
123+
:gsub('(%${[^}]+})', function(e) return e:gsub('[%.%[%]-]', '_') end) -- replace all dots/brackets/dash in placeholders (not allowed)
124+
end
125+
end
126+
127+
if not utils.safe_parse(body) then
128+
if is_user_snippet then
129+
prefix = type(prefix) == 'table' and table.concat(prefix, ',') or prefix
130+
vim.print(('[blink.cmp] Discard user snippet `%s` (%s), parsing failed!'):format(prefix, filetype))
107131
end
132+
return nil
108133
end
109134

110-
table.sort(tabstops, function(a, b) return a.index < b.index end)
111-
return tabstops
135+
return body:find('\n') and vim.split(body, '\n', { plain = true }) or body
112136
end
113137

114138
return utils

0 commit comments

Comments
 (0)