├── LICENSE ├── README.md └── lua └── snippets ├── config └── init.lua ├── init.lua └── utils ├── builtin.lua ├── cmp.lua └── init.lua /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-snippets 2 | 3 | Allow vscode style snippets to be used with native neovim snippets `vim.snippet`. Also comes with support for [friendly-snippets](https://github.com/rafamadriz/friendly-snippets). 4 | 5 | ## Features 6 | 7 | - Supports vscode style snippets 8 | - Has builtin support for [friendly-snippets](https://github.com/rafamadriz/friendly-snippets) 9 | - Uses `vim.snippet` under the hood for snippet expansion 10 | 11 | ## Requirements 12 | - Requires neovim >= 0.10 (with commit [f1775da](https://github.com/neovim/neovim/commit/f1775da07fe48da629468bcfcc2a8a6c4c3f40ed) or later) 13 | - (optional) [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) for completion support 14 | - (optional) [friendly-snippets](https://github.com/rafamadriz/friendly-snippets) for pre-built snippets 15 | 16 | ## Installation 17 | 18 | Using [lazy.nvim](https://github.com/folke/lazy.nvim) 19 | 20 | ```lua 21 | { 22 | "garymjr/nvim-snippets", 23 | keys = { 24 | { 25 | "", 26 | function() 27 | if vim.snippet.active({ direction = 1 }) then 28 | vim.schedule(function() 29 | vim.snippet.jump(1) 30 | end) 31 | return 32 | end 33 | return "" 34 | end, 35 | expr = true, 36 | silent = true, 37 | mode = "i", 38 | }, 39 | { 40 | "", 41 | function() 42 | vim.schedule(function() 43 | vim.snippet.jump(1) 44 | end) 45 | end, 46 | expr = true, 47 | silent = true, 48 | mode = "s", 49 | }, 50 | { 51 | "", 52 | function() 53 | if vim.snippet.active({ direction = -1 }) then 54 | vim.schedule(function() 55 | vim.snippet.jump(-1) 56 | end) 57 | return 58 | end 59 | return "" 60 | end, 61 | expr = true, 62 | silent = true, 63 | mode = { "i", "s" }, 64 | }, 65 | }, 66 | } 67 | ``` 68 | 69 | ## Configuration 70 | 71 | | Option | Type | Default | Description | 72 | -------------------|-----------|-------------------------------------------|------------------------ 73 | create_autocmd | `boolean?` | `false` | Optionally load all snippets when opening a file. Only needed if not using [nvim-cmp](https://github.com/hrsh7th/nvim-cmp). 74 | create_cmp_source | `boolean?` | `true` | Optionally create a [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) source. Source name will be `snippets`. 75 | friendly_snippets | `boolean?` | `false` | Set to true if using [friendly-snippets](https://github.com/rafamadriz/friendly-snippets). 76 | ignored_filetypes | `string[]?` | `nil` | Filetypes to ignore when loading snippets. 77 | extended_filetypes | `table?` | `nil` | Filetypes to load snippets for in addition to the default ones. `ex: {typescript = {'javascript'}}` 78 | global_snippets | `string[]?` | `{'all'}` | Snippets to load for all filetypes. 79 | search_paths | `string[]` | `{vim.fn.stdpath('config') .. '/snippets'}` | Paths to search for snippets. 80 | 81 | ## Example Snippet 82 | 83 | ```json 84 | { 85 | "Say hello to the world": { 86 | "prefix": ["hw", "hello"], 87 | "body": "Hello, ${1:world}!$0" 88 | } 89 | } 90 | ``` 91 | 92 | ## TODO 93 | - [ ] Automatically detect if friendly-snippets is installed 94 | - [X] Add support for friendly-snippets `package.json` definitions (#31) 95 | -------------------------------------------------------------------------------- /lua/snippets/config/init.lua: -------------------------------------------------------------------------------- 1 | local config = {} 2 | 3 | ---@class snippets.config.Options : snippets.config.DefaultOptions 4 | local C = {} 5 | 6 | ---@class snippets.config.DefaultOptions 7 | local defaults = { 8 | --- Should an autocmd be created to load snippets automatically? 9 | ---@type boolean 10 | create_autocmd = false, 11 | --- Wrap the preview text in a markdown code block for highlighting? 12 | ---@type boolean 13 | highlight_preview = true, 14 | --- Should the cmp source be created and registered? 15 | --- The created source name is "snippets" 16 | ---@type boolean 17 | create_cmp_source = true, 18 | --- Should we try to load the friendly-snippets snippets? 19 | ---@type boolean 20 | friendly_snippets = false, 21 | --- A list of filetypes to ignore snippets for 22 | ---@type table|nil 23 | ignored_filetypes = nil, 24 | --- A table of filetypes to apply additional snippets for 25 | --- example: { typescript = { "javascript" } } 26 | ---@type table|nil 27 | extended_filetypes = nil, 28 | --- A table of global snippets to load for all filetypes 29 | ---@type table|nil 30 | global_snippets = { "all" }, 31 | --- Paths to search for snippets 32 | ---@type string[] 33 | search_paths = { vim.fn.stdpath("config") .. "/snippets" }, 34 | } 35 | 36 | ---@type fun(opts?: snippets.config.Options): snippets.config.Options 37 | function config.new(opts) 38 | C = vim.tbl_extend("force", {}, defaults, opts or {}) 39 | return C 40 | end 41 | 42 | ---@type fun(): snippets.config.DefaultOptions 43 | function config.load_defaults() 44 | return defaults 45 | end 46 | 47 | ---@type fun(option: string, defaultValue?: any): any 48 | function config.get_option(option, defaultValue) 49 | return C[option] or defaultValue 50 | end 51 | 52 | ---@type fun(option: string, value: any) 53 | function config.set_option(option, value) 54 | C[option] = value 55 | end 56 | 57 | return config 58 | -------------------------------------------------------------------------------- /lua/snippets/init.lua: -------------------------------------------------------------------------------- 1 | local snippets = {} 2 | 3 | snippets.config = require("snippets.config") 4 | snippets.utils = require("snippets.utils") 5 | 6 | Snippets = snippets 7 | 8 | ---@class Snippet 9 | ---@field prefix string 10 | ---@field body string 11 | ---@field description? string 12 | 13 | --- Cached snippets for each language loaded 14 | ---@private 15 | ---@type table> 16 | snippets.cache = {} 17 | 18 | ---@private 19 | ---@type string|nil 20 | snippets.active_filetype = nil 21 | 22 | ---@private 23 | ---@type table> 24 | snippets.loaded_snippets = {} 25 | 26 | ---@private 27 | ---@type table 28 | snippets.registry = {} 29 | 30 | ---@param filetype string|nil 31 | function snippets.clear_cache(filetype) 32 | if filetype ~= nil then 33 | snippets.cache[filetype] = nil 34 | else 35 | snippets.cache = {} 36 | end 37 | end 38 | 39 | ---@type fun(filetype?: string): table|nil 40 | function snippets.load_snippets_for_ft(filetype) 41 | snippets.active_filetype = filetype 42 | if snippets.cache[filetype] then 43 | snippets.loaded_snippets = snippets.cache[filetype] 44 | return snippets.loaded_snippets 45 | end 46 | 47 | if snippets.utils.is_filetype_ignored(filetype) then 48 | return nil 49 | end 50 | 51 | local global_snippets = snippets.utils.get_global_snippets() 52 | local extended_snippets = snippets.utils.get_extended_snippets(filetype) 53 | local ft_snippets = snippets.utils.get_snippets_for_ft(filetype) 54 | snippets.loaded_snippets = vim.tbl_deep_extend("force", {}, global_snippets, extended_snippets, ft_snippets) 55 | snippets.cache[filetype] = vim.deepcopy(snippets.loaded_snippets) 56 | 57 | return snippets.loaded_snippets 58 | end 59 | 60 | ---@return table 61 | function snippets.get_loaded_snippets() 62 | return snippets.loaded_snippets 63 | end 64 | 65 | ---@param opts? table -- Make a better type for this 66 | function snippets.setup(opts) 67 | snippets.config.new(opts) 68 | if snippets.config.get_option("friendly_snippets") then 69 | snippets.utils.load_friendly_snippets() 70 | end 71 | 72 | snippets.utils.register_snippets() 73 | 74 | if snippets.config.get_option("create_autocmd") then 75 | snippets.utils.create_autocmd() 76 | end 77 | 78 | if snippets.config.get_option("create_cmp_source") then 79 | snippets.utils.register_cmp_source() 80 | end 81 | end 82 | 83 | return snippets 84 | -------------------------------------------------------------------------------- /lua/snippets/utils/builtin.lua: -------------------------------------------------------------------------------- 1 | -- credit to https://github.com/L3MON4D3 for these variables 2 | -- see: https://github.com/L3MON4D3/LuaSnip/blob/master/lua/luasnip/util/_builtin_vars.lua 3 | 4 | local builtin = { 5 | lazy = {}, 6 | } 7 | 8 | function builtin.lazy.TM_FILENAME() 9 | return vim.fn.expand("%:t") 10 | end 11 | 12 | function builtin.lazy.TM_FILENAME_BASE() 13 | return vim.fn.expand("%:t:s?\\.[^\\.]\\+$??") 14 | end 15 | 16 | function builtin.lazy.TM_DIRECTORY() 17 | return vim.fn.expand("%:p:h") 18 | end 19 | 20 | function builtin.lazy.TM_FILEPATH() 21 | return vim.fn.expand("%:p") 22 | end 23 | 24 | function builtin.lazy.CLIPBOARD() 25 | return vim.fn.getreg(vim.v.register, true) 26 | end 27 | 28 | local function buf_to_ws_part() 29 | local LSP_WORSKPACE_PARTS = "LSP_WORSKPACE_PARTS" 30 | local ok, ws_parts = pcall(vim.api.nvim_buf_get_var, 0, LSP_WORSKPACE_PARTS) 31 | if not ok then 32 | local file_path = vim.fn.expand("%:p") 33 | 34 | for _, ws in pairs(vim.lsp.buf.list_workspace_folders()) do 35 | if file_path:find(ws, 1, true) == 1 then 36 | ws_parts = { ws, file_path:sub(#ws + 2, -1) } 37 | break 38 | end 39 | end 40 | -- If it can't be extracted from lsp, then we use the file path 41 | if not ok and not ws_parts then 42 | ws_parts = { vim.fn.expand("%:p:h"), vim.fn.expand("%:p:t") } 43 | end 44 | vim.api.nvim_buf_set_var(0, LSP_WORSKPACE_PARTS, ws_parts) 45 | end 46 | return ws_parts 47 | end 48 | 49 | function builtin.lazy.RELATIVE_FILEPATH() -- The relative (to the opened workspace or folder) file path of the current document 50 | return buf_to_ws_part()[2] 51 | end 52 | 53 | function builtin.lazy.WORKSPACE_FOLDER() -- The path of the opened workspace or folder 54 | return buf_to_ws_part()[1] 55 | end 56 | 57 | function builtin.lazy.WORKSPACE_NAME() -- The name of the opened workspace or folder 58 | local parts = vim.split(buf_to_ws_part()[1] or "", "[\\/]") 59 | return parts[#parts] 60 | end 61 | 62 | function builtin.lazy.CURRENT_YEAR() 63 | return os.date("%Y") 64 | end 65 | 66 | function builtin.lazy.CURRENT_YEAR_SHORT() 67 | return os.date("%y") 68 | end 69 | 70 | function builtin.lazy.CURRENT_MONTH() 71 | return os.date("%m") 72 | end 73 | 74 | function builtin.lazy.CURRENT_MONTH_NAME() 75 | return os.date("%B") 76 | end 77 | 78 | function builtin.lazy.CURRENT_MONTH_NAME_SHORT() 79 | return os.date("%b") 80 | end 81 | 82 | function builtin.lazy.CURRENT_DATE() 83 | return os.date("%d") 84 | end 85 | 86 | function builtin.lazy.CURRENT_DAY_NAME() 87 | return os.date("%A") 88 | end 89 | 90 | function builtin.lazy.CURRENT_DAY_NAME_SHORT() 91 | return os.date("%a") 92 | end 93 | 94 | function builtin.lazy.CURRENT_HOUR() 95 | return os.date("%H") 96 | end 97 | 98 | function builtin.lazy.CURRENT_MINUTE() 99 | return os.date("%M") 100 | end 101 | 102 | function builtin.lazy.CURRENT_SECOND() 103 | return os.date("%S") 104 | end 105 | 106 | function builtin.lazy.CURRENT_SECONDS_UNIX() 107 | return tostring(os.time()) 108 | end 109 | 110 | local function get_timezone_offset(ts) 111 | local utcdate = os.date("!*t", ts) 112 | local localdate = os.date("*t", ts) 113 | localdate.isdst = false -- this is the trick 114 | local diff = os.difftime(os.time(localdate), os.time(utcdate)) 115 | local h, m = math.modf(diff / 3600) 116 | return string.format("%+.4d", 100 * h + 60 * m) 117 | end 118 | 119 | function builtin.lazy.CURRENT_TIMEZONE_OFFSET() 120 | return get_timezone_offset(os.time()):gsub("([+-])(%d%d)(%d%d)$", "%1%2:%3") 121 | end 122 | 123 | math.randomseed(os.time()) 124 | 125 | function builtin.lazy.RANDOM() 126 | return string.format("%06d", math.random(999999)) 127 | end 128 | 129 | function builtin.lazy.RANDOM_HEX() 130 | return string.format("%06x", math.random(16777216)) --16^6 131 | end 132 | 133 | function builtin.lazy.UUID() 134 | local random = math.random 135 | local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" 136 | local out 137 | local function subs(c) 138 | local v = (((c == "x") and random(0, 15)) or random(8, 11)) 139 | return string.format("%x", v) 140 | end 141 | 142 | out = template:gsub("[xy]", subs) 143 | return out 144 | end 145 | 146 | local _comments_cache = {} 147 | local function buffer_comment_chars() 148 | local commentstring = vim.bo.commentstring 149 | if _comments_cache[commentstring] then 150 | return _comments_cache[commentstring] 151 | end 152 | local comments = { "//", "/*", "*/" } 153 | local placeholder = "%s" 154 | local index_placeholder = commentstring:find(vim.pesc(placeholder)) 155 | if index_placeholder then 156 | index_placeholder = index_placeholder - 1 157 | if index_placeholder + #placeholder == #commentstring then 158 | comments[1] = vim.trim(commentstring:sub(1, -#placeholder - 1)) 159 | else 160 | comments[2] = vim.trim(commentstring:sub(1, index_placeholder)) 161 | comments[3] = vim.trim(commentstring:sub(index_placeholder + #placeholder + 1, -1)) 162 | end 163 | end 164 | _comments_cache[commentstring] = comments 165 | return comments 166 | end 167 | 168 | function builtin.lazy.LINE_COMMENT() 169 | return buffer_comment_chars()[1] 170 | end 171 | 172 | function builtin.lazy.BLOCK_COMMENT_START() 173 | return buffer_comment_chars()[2] 174 | end 175 | 176 | function builtin.lazy.BLOCK_COMMENT_END() 177 | return buffer_comment_chars()[3] 178 | end 179 | 180 | local function get_cursor() 181 | local c = vim.api.nvim_win_get_cursor(0) 182 | c[1] = c[1] - 1 183 | return c 184 | end 185 | 186 | local function get_current_line() 187 | local pos = get_cursor() 188 | return vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)[1] 189 | end 190 | 191 | local function word_under_cursor(cur, line) 192 | if line == nil then 193 | return 194 | end 195 | 196 | local ind_start = 1 197 | local ind_end = #line 198 | 199 | while true do 200 | local tmp = string.find(line, "%W%w", ind_start) 201 | if not tmp then 202 | break 203 | end 204 | if tmp > cur[2] + 1 then 205 | break 206 | end 207 | ind_start = tmp + 1 208 | end 209 | 210 | local tmp = string.find(line, "%w%W", cur[2] + 1) 211 | if tmp then 212 | ind_end = tmp 213 | end 214 | 215 | return string.sub(line, ind_start, ind_end) 216 | end 217 | 218 | local function get_selected_text() 219 | if vim.fn.visualmode() == "V" then 220 | return vim.fn.trim(vim.fn.getreg(vim.v.register, true), "\n", 2) 221 | end 222 | return "" 223 | end 224 | 225 | vim.api.nvim_create_autocmd("InsertEnter", { 226 | group = vim.api.nvim_create_augroup("nvim_snippets_eager_enter", { clear = true }), 227 | callback = function() 228 | builtin.eager = {} 229 | builtin.eager.TM_CURRENT_LINE = get_current_line() 230 | builtin.eager.TM_CURRENT_WORD = word_under_cursor(get_cursor(), builtin.eager.TM_CURRENT_LINE) 231 | builtin.eager.TM_LINE_INDEX = tostring(get_cursor()[1]) 232 | builtin.eager.TM_LINE_NUMBER = tostring(get_cursor()[1] + 1) 233 | builtin.eager.TM_SELECTED_TEXT = get_selected_text() 234 | end, 235 | }) 236 | 237 | vim.api.nvim_create_autocmd("InsertLeave", { 238 | group = vim.api.nvim_create_augroup("nvim_snippets_eager_leave", { clear = true }), 239 | callback = function() 240 | builtin.eager = nil 241 | end, 242 | }) 243 | 244 | return builtin 245 | -------------------------------------------------------------------------------- /lua/snippets/utils/cmp.lua: -------------------------------------------------------------------------------- 1 | local cmp = require("cmp") 2 | local utils = require("snippets.utils") 3 | 4 | local source = {} 5 | 6 | source.new = function() 7 | return setmetatable({}, { __index = source }) 8 | end 9 | 10 | source.get_keyword_pattern = function() 11 | return "\\%([^[:alnum:][:blank:]]\\|\\w\\+\\)" 12 | end 13 | 14 | function source:is_available() 15 | local ok, _ = pcall(require, "snippets") 16 | return ok 17 | end 18 | 19 | function source:get_debug_name() 20 | return "snippets" 21 | end 22 | 23 | function source:complete(_, callback) 24 | local loaded_snippets = Snippets.load_snippets_for_ft(vim.bo.filetype) 25 | 26 | if loaded_snippets == nil then 27 | return 28 | end 29 | 30 | local response = {} 31 | 32 | for key in pairs(loaded_snippets) do 33 | local snippet = loaded_snippets[key] 34 | local body 35 | if type(snippet.body) == "table" then 36 | body = table.concat(snippet.body, "\n") 37 | else 38 | body = snippet.body 39 | end 40 | 41 | local prefix = loaded_snippets[key].prefix 42 | if type(prefix) == "table" then 43 | for _, p in ipairs(prefix) do 44 | table.insert(response, { 45 | label = p, 46 | kind = cmp.lsp.CompletionItemKind.Snippet, 47 | insertTextFormat = cmp.lsp.InsertTextFormat.Snippet, 48 | insertTextMode = cmp.lsp.InsertTextMode.AdjustIndentation, 49 | insertText = body, 50 | data = { 51 | prefix = p, 52 | body = body, 53 | }, 54 | }) 55 | end 56 | else 57 | table.insert(response, { 58 | label = prefix, 59 | kind = cmp.lsp.CompletionItemKind.Snippet, 60 | insertTextFormat = cmp.lsp.InsertTextFormat.Snippet, 61 | insertTextMode = cmp.lsp.InsertTextMode.AdjustIndentation, 62 | insertText = body, 63 | data = { 64 | prefix = prefix, 65 | body = body, 66 | }, 67 | }) 68 | end 69 | end 70 | callback(response) 71 | end 72 | 73 | function source:resolve(completion_item, callback) 74 | -- highlight code block 75 | local preview = utils.preview(completion_item.data.body) 76 | if require("snippets.config").get_option("highlight_preview", false) then 77 | preview = string.format("```%s\n%s\n```", vim.bo.filetype, preview) 78 | end 79 | completion_item.documentation = { 80 | kind = cmp.lsp.MarkupKind.Markdown, 81 | value = preview, 82 | } 83 | 84 | completion_item.insertText = Snippets.utils.expand_vars(completion_item.data.body) 85 | callback(completion_item) 86 | end 87 | 88 | function source:execute(completion_item, callback) 89 | callback(completion_item) 90 | end 91 | 92 | local function register() 93 | cmp.register_source("snippets", source) 94 | end 95 | 96 | return { register = register } 97 | -------------------------------------------------------------------------------- /lua/snippets/utils/init.lua: -------------------------------------------------------------------------------- 1 | local utils = {} 2 | 3 | utils.builtin_vars = require("snippets.utils.builtin") 4 | 5 | ---@type fun(snippet: Snippet, fallback: string): table 6 | local function read_snippet(snippet, fallback) 7 | local snippets = {} 8 | local prefix = snippet.prefix or fallback 9 | local description = snippet.description or fallback 10 | local body = snippet.body 11 | if type(prefix) == "table" then 12 | for _, p in ipairs(prefix) do 13 | snippets[p] = { 14 | prefix = p, 15 | body = body, 16 | description = description, 17 | } 18 | end 19 | else 20 | snippets[prefix] = { 21 | prefix = prefix, 22 | body = body, 23 | description = description, 24 | } 25 | end 26 | return snippets 27 | end 28 | 29 | ---@type fun(path: string): string|nil 30 | local function read_file(path) 31 | local file = io.open(path, "r") 32 | if not file then 33 | return nil 34 | end 35 | local content = file:read("*a") 36 | file:close() 37 | return content 38 | end 39 | 40 | ---@type fun(filetype: string): boolean 41 | function utils.is_filetype_ignored(filetype) 42 | local ignored_filetypes = Snippets.config.get_option("ignored_filetypes", {}) 43 | return vim.tbl_contains(ignored_filetypes, filetype) 44 | end 45 | 46 | ---@type fun(value: string|string[]): string[] 47 | function utils.normalize_table(value) 48 | if type(value) == "table" then 49 | return value 50 | end 51 | 52 | local tbl = {} 53 | table.insert(tbl, value) 54 | return tbl 55 | end 56 | 57 | ---@type fun(dir: string, result?: string[]): string[] 58 | ---@return string[] 59 | function utils.scan_for_snippets(dir, result) 60 | result = result or {} 61 | 62 | local stat = vim.uv.fs_stat(dir) 63 | if not stat then 64 | return result 65 | end 66 | 67 | if stat.type == "directory" then 68 | local req = vim.uv.fs_scandir(dir) 69 | if not req then 70 | return result 71 | end 72 | 73 | local function iter() 74 | return vim.uv.fs_scandir_next(req) 75 | end 76 | 77 | for name, ftype in iter do 78 | local path = string.format("%s/%s", dir, name) 79 | 80 | if ftype == "directory" then 81 | result[name] = utils.scan_for_snippets(path, result[name] or {}) 82 | else 83 | utils.scan_for_snippets(path, result) 84 | end 85 | end 86 | elseif stat.type == "file" then 87 | local name = vim.fn.fnamemodify(dir, ":t") 88 | 89 | if name:match("%.json$") then 90 | table.insert(result, dir) 91 | end 92 | elseif stat.type == "link" then 93 | local target = vim.uv.fs_readlink(dir) 94 | 95 | if target then 96 | utils.scan_for_snippets(target, result) 97 | end 98 | end 99 | 100 | return result 101 | end 102 | 103 | function utils.register_snippets() 104 | local search_paths = Snippets.config.get_option("search_paths", {}) 105 | 106 | for _, path in ipairs(search_paths) do 107 | local files = utils.load_package_json(path) or utils.scan_for_snippets(path) 108 | for ft, file in pairs(files) do 109 | local key 110 | if type(ft) == "number" then 111 | key = vim.fn.fnamemodify(files[ft], ":t:r") 112 | else 113 | key = ft 114 | end 115 | 116 | if not key then 117 | return 118 | end 119 | 120 | Snippets.registry[key] = Snippets.registry[key] or {} 121 | if type(file) == "table" then 122 | vim.list_extend(Snippets.registry[key], file) 123 | else 124 | table.insert(Snippets.registry[key], file) 125 | end 126 | end 127 | end 128 | end 129 | 130 | --- This will try to load the snippets from the package.json file 131 | ---@param path string 132 | function utils.load_package_json(path) 133 | local file = path .. "/package.json" 134 | local data = read_file(file) 135 | 136 | if not data then 137 | return 138 | end 139 | 140 | local pkg = vim.json.decode(data) 141 | ---@type {path: string, language: string|string[]}[] 142 | local snippets = vim.tbl_get(pkg, "contributes", "snippets") 143 | 144 | if not snippets then 145 | return 146 | end 147 | 148 | local ret = {} ---@type table 149 | for _, s in ipairs(snippets) do 150 | local langs = s.language or {} 151 | langs = type(langs) == "string" and { langs } or langs 152 | ---@cast langs string[] 153 | for _, lang in ipairs(langs) do 154 | ret[lang] = ret[lang] or {} 155 | table.insert(ret[lang], vim.fs.normalize(vim.fs.joinpath(path, s.path))) 156 | end 157 | end 158 | return ret 159 | end 160 | 161 | ---@type fun(path: string, silent?: boolean) 162 | function utils.reload_file(path, silent) 163 | local contents = read_file(path) 164 | if contents then 165 | local reloaded_snippets = vim.json.decode(contents) 166 | for _, key in ipairs(vim.tbl_keys(reloaded_snippets)) do 167 | Snippets.loaded_snippets = 168 | vim.tbl_deep_extend("force", {}, Snippets.loaded_snippets, read_snippet(reloaded_snippets[key], key)) 169 | end 170 | Snippets.loaded_snippets = vim.tbl_deep_extend("force", {}, Snippets.loaded_snippets, reloaded_snippets) 171 | if not silent then 172 | vim.notify(string.format("Reloaded %d snippets", #vim.tbl_keys(reloaded_snippets), vim.log.levels.INFO)) 173 | end 174 | end 175 | Snippets.clear_cache() -- clear full cache to catch edge cases, such as #47 176 | end 177 | 178 | ---@deprecated 179 | ---@type fun(filetype: string, files?: string[]): string[] 180 | function utils.get_filetype(filetype, files) 181 | files = files or {} 182 | local ft_files = Snippets.registry[filetype] 183 | if type(ft_files) == "table" then 184 | for _, f in ipairs(ft_files) do 185 | table.insert(files, f) 186 | end 187 | else 188 | table.insert(files, ft_files) 189 | end 190 | return files 191 | end 192 | 193 | ---@type fun(filetype: string): boolean 194 | function utils.is_filetype_extended(filetype) 195 | if vim.tbl_contains(Snippets.config.get_option("extended_filetypes", {}), filetype) then 196 | return true 197 | end 198 | return false 199 | end 200 | 201 | ---@type fun(filetype?: string): table 202 | function utils.get_snippets_for_ft(filetype) 203 | local loaded_snippets = {} 204 | local files = Snippets.registry[filetype] 205 | if not files then 206 | return loaded_snippets 207 | end 208 | 209 | if type(files) == "table" then 210 | for _, f in ipairs(files) do 211 | local contents = read_file(f) 212 | if contents then 213 | local snippets = vim.json.decode(contents) 214 | for _, key in ipairs(vim.tbl_keys(snippets)) do 215 | local snippet = read_snippet(snippets[key], key) 216 | loaded_snippets = vim.tbl_deep_extend("force", {}, loaded_snippets, snippet) 217 | end 218 | end 219 | end 220 | else 221 | local contents = read_file(files) 222 | if contents then 223 | local snippets = vim.json.decode(contents) 224 | for _, key in ipairs(vim.tbl_keys(snippets)) do 225 | local snippet = read_snippet(snippets[key], key) 226 | loaded_snippets = vim.tbl_deep_extend("force", {}, loaded_snippets, snippet) 227 | end 228 | end 229 | end 230 | 231 | return loaded_snippets 232 | end 233 | 234 | ---@type fun(filetype: string): table 235 | function utils.get_extended_snippets(filetype) 236 | local loaded_snippets = {} 237 | if not filetype then 238 | return loaded_snippets 239 | end 240 | 241 | local extended_snippets = Snippets.config.get_option("extended_filetypes", {})[filetype] or {} 242 | for _, ft in ipairs(extended_snippets) do 243 | if utils.is_filetype_extended(ft) then 244 | loaded_snippets = vim.tbl_deep_extend("force", {}, loaded_snippets, utils.get_extended_snippets(ft)) 245 | else 246 | loaded_snippets = vim.tbl_deep_extend("force", {}, loaded_snippets, utils.get_snippets_for_ft(ft)) 247 | end 248 | end 249 | return loaded_snippets 250 | end 251 | 252 | ---@return table 253 | function utils.get_global_snippets() 254 | local loaded_snippets = {} 255 | local global_snippets = Snippets.config.get_option("global_snippets", {}) 256 | for _, ft in ipairs(global_snippets) do 257 | if utils.is_filetype_extended(ft) then 258 | loaded_snippets = vim.tbl_deep_extend("force", {}, loaded_snippets, utils.get_extended_snippets(ft)) 259 | else 260 | loaded_snippets = vim.tbl_deep_extend("force", {}, loaded_snippets, utils.get_snippets_for_ft(ft)) 261 | end 262 | end 263 | return loaded_snippets 264 | end 265 | 266 | ---@type fun(input: string): vim.snippet.Node|nil 267 | local function safe_parse(input) 268 | local safe, parsed = pcall(vim.lsp._snippet_grammar.parse, input) 269 | if not safe then 270 | return nil 271 | end 272 | return parsed 273 | end 274 | 275 | function utils.preview(snippet) 276 | local parse = safe_parse(utils.expand_vars(snippet)) 277 | return parse and tostring(parse) or snippet 278 | end 279 | 280 | ---@type fun(snippet: string): string 281 | function utils.expand_vars(input) 282 | local lazy_vars = Snippets.utils.builtin_vars.lazy 283 | local eager_vars = Snippets.utils.builtin_vars.eager or {} 284 | 285 | local resolved_snippet = input 286 | local parsed_snippet = safe_parse(input) 287 | if not parsed_snippet then 288 | return input 289 | end 290 | 291 | for _, child in ipairs(parsed_snippet.data.children) do 292 | local type, data = child.type, child.data 293 | if type == vim.lsp._snippet_grammar.NodeType.Variable then 294 | if eager_vars[data.name] then 295 | resolved_snippet = resolved_snippet:gsub("%$[{]?(" .. data.name .. ")[}]?", eager_vars[data.name]) 296 | elseif lazy_vars[data.name] then 297 | resolved_snippet = resolved_snippet:gsub("%$[{]?(" .. data.name .. ")[}]?", lazy_vars[data.name]()) 298 | end 299 | end 300 | end 301 | 302 | return resolved_snippet 303 | end 304 | 305 | function utils.create_autocmd() 306 | if not Snippets.config.get_option("create_autocmd") then 307 | return 308 | end 309 | 310 | vim.api.nvim_create_autocmd("FileType", { 311 | group = vim.api.nvim_create_augroup("snippets_ft_detect", { clear = true }), 312 | pattern = "*", 313 | callback = function() 314 | Snippets.load_snippets_for_ft(vim.bo.filetype) 315 | end, 316 | }) 317 | end 318 | 319 | function utils.register_cmp_source() 320 | require("snippets.utils.cmp").register() 321 | end 322 | 323 | function utils.load_friendly_snippets() 324 | local search_paths = Snippets.config.get_option("search_paths", {}) 325 | for _, path in ipairs(vim.api.nvim_list_runtime_paths()) do 326 | if string.match(path, "friendly.snippets") then 327 | table.insert(search_paths, path) 328 | end 329 | end 330 | Snippets.config.set_option("search_paths", search_paths) 331 | end 332 | 333 | ---@type fun(prefix: string): table|nil 334 | function utils.find_snippet_prefix(prefix) 335 | if not prefix then 336 | return nil 337 | end 338 | 339 | return Snippets.loaded_snippets[prefix] 340 | end 341 | 342 | return utils 343 | --------------------------------------------------------------------------------