cmp.lua 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. local M = {}
  2. M.methods = {}
  3. ---checks if the character preceding the cursor is a space character
  4. ---@return boolean true if it is a space character, false otherwise
  5. local check_backspace = function()
  6. local col = vim.fn.col "." - 1
  7. return col == 0 or vim.fn.getline("."):sub(col, col):match "%s"
  8. end
  9. M.methods.check_backspace = check_backspace
  10. local function T(str)
  11. return vim.api.nvim_replace_termcodes(str, true, true, true)
  12. end
  13. ---wraps vim.fn.feedkeys while replacing key codes with escape codes
  14. ---Ex: feedkeys("<CR>", "n") becomes feedkeys("^M", "n")
  15. ---@param key string
  16. ---@param mode string
  17. local function feedkeys(key, mode)
  18. vim.fn.feedkeys(T(key), mode)
  19. end
  20. M.methods.feedkeys = feedkeys
  21. ---checks if emmet_ls is available and active in the buffer
  22. ---@return boolean true if available, false otherwise
  23. local is_emmet_active = function()
  24. local clients = vim.lsp.buf_get_clients()
  25. for _, client in pairs(clients) do
  26. if client.name == "emmet_ls" then
  27. return true
  28. end
  29. end
  30. return false
  31. end
  32. M.methods.is_emmet_active = is_emmet_active
  33. ---when inside a snippet, seeks to the nearest luasnip field if possible, and checks if it is jumpable
  34. ---@param dir number 1 for forward, -1 for backward; defaults to 1
  35. ---@return boolean true if a jumpable luasnip field is found while inside a snippet
  36. local function jumpable(dir)
  37. local luasnip_ok, luasnip = pcall(require, "luasnip")
  38. if not luasnip_ok then
  39. return
  40. end
  41. local win_get_cursor = vim.api.nvim_win_get_cursor
  42. local get_current_buf = vim.api.nvim_get_current_buf
  43. local function inside_snippet()
  44. -- for outdated versions of luasnip
  45. if not luasnip.session.current_nodes then
  46. return false
  47. end
  48. local node = luasnip.session.current_nodes[get_current_buf()]
  49. if not node then
  50. return false
  51. end
  52. local snip_begin_pos, snip_end_pos = node.parent.snippet.mark:pos_begin_end()
  53. local pos = win_get_cursor(0)
  54. pos[1] = pos[1] - 1 -- LuaSnip is 0-based not 1-based like nvim for rows
  55. return pos[1] >= snip_begin_pos[1] and pos[1] <= snip_end_pos[1]
  56. end
  57. ---sets the current buffer's luasnip to the one nearest the cursor
  58. ---@return boolean true if a node is found, false otherwise
  59. local function seek_luasnip_cursor_node()
  60. -- for outdated versions of luasnip
  61. if not luasnip.session.current_nodes then
  62. return false
  63. end
  64. local pos = win_get_cursor(0)
  65. pos[1] = pos[1] - 1
  66. local node = luasnip.session.current_nodes[get_current_buf()]
  67. if not node then
  68. return false
  69. end
  70. local snippet = node.parent.snippet
  71. local exit_node = snippet.insert_nodes[0]
  72. -- exit early if we're past the exit node
  73. if exit_node then
  74. local exit_pos_end = exit_node.mark:pos_end()
  75. if (pos[1] > exit_pos_end[1]) or (pos[1] == exit_pos_end[1] and pos[2] > exit_pos_end[2]) then
  76. snippet:remove_from_jumplist()
  77. luasnip.session.current_nodes[get_current_buf()] = nil
  78. return false
  79. end
  80. end
  81. node = snippet.inner_first:jump_into(1, true)
  82. while node ~= nil and node.next ~= nil and node ~= snippet do
  83. local n_next = node.next
  84. local next_pos = n_next and n_next.mark:pos_begin()
  85. local candidate = n_next ~= snippet and next_pos and (pos[1] < next_pos[1])
  86. or (pos[1] == next_pos[1] and pos[2] < next_pos[2])
  87. -- Past unmarked exit node, exit early
  88. if n_next == nil or n_next == snippet.next then
  89. snippet:remove_from_jumplist()
  90. luasnip.session.current_nodes[get_current_buf()] = nil
  91. return false
  92. end
  93. if candidate then
  94. luasnip.session.current_nodes[get_current_buf()] = node
  95. return true
  96. end
  97. local ok
  98. ok, node = pcall(node.jump_from, node, 1, true) -- no_move until last stop
  99. if not ok then
  100. snippet:remove_from_jumplist()
  101. luasnip.session.current_nodes[get_current_buf()] = nil
  102. return false
  103. end
  104. end
  105. -- No candidate, but have an exit node
  106. if exit_node then
  107. -- to jump to the exit node, seek to snippet
  108. luasnip.session.current_nodes[get_current_buf()] = snippet
  109. return true
  110. end
  111. -- No exit node, exit from snippet
  112. snippet:remove_from_jumplist()
  113. luasnip.session.current_nodes[get_current_buf()] = nil
  114. return false
  115. end
  116. if dir == -1 then
  117. return inside_snippet() and luasnip.jumpable(-1)
  118. else
  119. return inside_snippet() and seek_luasnip_cursor_node() and luasnip.jumpable()
  120. end
  121. end
  122. M.methods.jumpable = jumpable
  123. M.config = function()
  124. local status_cmp_ok, cmp = pcall(require, "cmp")
  125. if not status_cmp_ok then
  126. return
  127. end
  128. local status_luasnip_ok, luasnip = pcall(require, "luasnip")
  129. if not status_luasnip_ok then
  130. return
  131. end
  132. lvim.builtin.cmp = {
  133. confirm_opts = {
  134. behavior = cmp.ConfirmBehavior.Replace,
  135. select = false,
  136. },
  137. completion = {
  138. ---@usage The minimum length of a word to complete on.
  139. keyword_length = 1,
  140. },
  141. experimental = {
  142. ghost_text = true,
  143. native_menu = false,
  144. },
  145. formatting = {
  146. fields = { "kind", "abbr", "menu" },
  147. kind_icons = {
  148. Class = " ",
  149. Color = " ",
  150. Constant = "ﲀ ",
  151. Constructor = " ",
  152. Enum = "練",
  153. EnumMember = " ",
  154. Event = " ",
  155. Field = " ",
  156. File = "",
  157. Folder = " ",
  158. Function = " ",
  159. Interface = "ﰮ ",
  160. Keyword = " ",
  161. Method = " ",
  162. Module = " ",
  163. Operator = "",
  164. Property = " ",
  165. Reference = " ",
  166. Snippet = " ",
  167. Struct = " ",
  168. Text = " ",
  169. TypeParameter = " ",
  170. Unit = "塞",
  171. Value = " ",
  172. Variable = " ",
  173. },
  174. source_names = {
  175. nvim_lsp = "(LSP)",
  176. emoji = "(Emoji)",
  177. path = "(Path)",
  178. calc = "(Calc)",
  179. cmp_tabnine = "(Tabnine)",
  180. vsnip = "(Snippet)",
  181. luasnip = "(Snippet)",
  182. buffer = "(Buffer)",
  183. },
  184. duplicates = {
  185. buffer = 1,
  186. path = 1,
  187. nvim_lsp = 0,
  188. luasnip = 1,
  189. },
  190. duplicates_default = 0,
  191. format = function(entry, vim_item)
  192. vim_item.kind = lvim.builtin.cmp.formatting.kind_icons[vim_item.kind]
  193. vim_item.menu = lvim.builtin.cmp.formatting.source_names[entry.source.name]
  194. vim_item.dup = lvim.builtin.cmp.formatting.duplicates[entry.source.name]
  195. or lvim.builtin.cmp.formatting.duplicates_default
  196. return vim_item
  197. end,
  198. },
  199. snippet = {
  200. expand = function(args)
  201. require("luasnip").lsp_expand(args.body)
  202. end,
  203. },
  204. documentation = {
  205. border = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" },
  206. },
  207. sources = {
  208. { name = "nvim_lsp" },
  209. { name = "path" },
  210. { name = "luasnip" },
  211. { name = "cmp_tabnine" },
  212. { name = "nvim_lua" },
  213. { name = "buffer" },
  214. { name = "calc" },
  215. { name = "emoji" },
  216. { name = "treesitter" },
  217. { name = "crates" },
  218. },
  219. mapping = {
  220. ["<C-k>"] = cmp.mapping.select_prev_item(),
  221. ["<C-j>"] = cmp.mapping.select_next_item(),
  222. ["<C-d>"] = cmp.mapping.scroll_docs(-4),
  223. ["<C-f>"] = cmp.mapping.scroll_docs(4),
  224. -- TODO: potentially fix emmet nonsense
  225. ["<Tab>"] = cmp.mapping(function(fallback)
  226. if cmp.visible() then
  227. cmp.select_next_item()
  228. elseif luasnip.expandable() then
  229. luasnip.expand()
  230. elseif jumpable() then
  231. luasnip.jump(1)
  232. elseif check_backspace() then
  233. fallback()
  234. elseif is_emmet_active() then
  235. return vim.fn["cmp#complete"]()
  236. else
  237. fallback()
  238. end
  239. end, {
  240. "i",
  241. "s",
  242. }),
  243. ["<S-Tab>"] = cmp.mapping(function(fallback)
  244. if cmp.visible() then
  245. cmp.select_prev_item()
  246. elseif jumpable(-1) then
  247. luasnip.jump(-1)
  248. else
  249. fallback()
  250. end
  251. end, {
  252. "i",
  253. "s",
  254. }),
  255. ["<C-Space>"] = cmp.mapping.complete(),
  256. ["<C-e>"] = cmp.mapping.abort(),
  257. ["<CR>"] = cmp.mapping(function(fallback)
  258. if cmp.visible() and cmp.confirm(lvim.builtin.cmp.confirm_opts) then
  259. if jumpable() then
  260. luasnip.jump(1)
  261. end
  262. return
  263. end
  264. if jumpable() then
  265. if not luasnip.jump(1) then
  266. fallback()
  267. end
  268. else
  269. fallback()
  270. end
  271. end),
  272. },
  273. }
  274. end
  275. M.setup = function()
  276. require("luasnip/loaders/from_vscode").lazy_load()
  277. require("cmp").setup(lvim.builtin.cmp)
  278. end
  279. return M