cmp.lua 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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. max_width = 0,
  148. kind_icons = {
  149. Class = " ",
  150. Color = " ",
  151. Constant = "ﲀ ",
  152. Constructor = " ",
  153. Enum = "練",
  154. EnumMember = " ",
  155. Event = " ",
  156. Field = " ",
  157. File = "",
  158. Folder = " ",
  159. Function = " ",
  160. Interface = "ﰮ ",
  161. Keyword = " ",
  162. Method = " ",
  163. Module = " ",
  164. Operator = "",
  165. Property = " ",
  166. Reference = " ",
  167. Snippet = " ",
  168. Struct = " ",
  169. Text = " ",
  170. TypeParameter = " ",
  171. Unit = "塞",
  172. Value = " ",
  173. Variable = " ",
  174. },
  175. source_names = {
  176. nvim_lsp = "(LSP)",
  177. emoji = "(Emoji)",
  178. path = "(Path)",
  179. calc = "(Calc)",
  180. cmp_tabnine = "(Tabnine)",
  181. vsnip = "(Snippet)",
  182. luasnip = "(Snippet)",
  183. buffer = "(Buffer)",
  184. },
  185. duplicates = {
  186. buffer = 1,
  187. path = 1,
  188. nvim_lsp = 0,
  189. luasnip = 1,
  190. },
  191. duplicates_default = 0,
  192. format = function(entry, vim_item)
  193. local max_width = lvim.builtin.cmp.formatting.max_width
  194. if max_width ~= 0 and #vim_item.abbr > max_width then
  195. vim_item.abbr = string.sub(vim_item.abbr, 1, max_width - 1) .. "…"
  196. end
  197. if lvim.use_icons then
  198. vim_item.kind = lvim.builtin.cmp.formatting.kind_icons[vim_item.kind]
  199. end
  200. vim_item.menu = lvim.builtin.cmp.formatting.source_names[entry.source.name]
  201. vim_item.dup = lvim.builtin.cmp.formatting.duplicates[entry.source.name]
  202. or lvim.builtin.cmp.formatting.duplicates_default
  203. return vim_item
  204. end,
  205. },
  206. snippet = {
  207. expand = function(args)
  208. require("luasnip").lsp_expand(args.body)
  209. end,
  210. },
  211. window = {
  212. completion = cmp.config.window.bordered(),
  213. documentation = cmp.config.window.bordered(),
  214. },
  215. sources = {
  216. { name = "nvim_lsp" },
  217. { name = "path" },
  218. { name = "luasnip" },
  219. { name = "cmp_tabnine" },
  220. { name = "nvim_lua" },
  221. { name = "buffer" },
  222. { name = "calc" },
  223. { name = "emoji" },
  224. { name = "treesitter" },
  225. { name = "crates" },
  226. },
  227. mapping = cmp.mapping.preset.insert {
  228. ["<C-k>"] = cmp.mapping.select_prev_item(),
  229. ["<C-j>"] = cmp.mapping.select_next_item(),
  230. ["<C-d>"] = cmp.mapping.scroll_docs(-4),
  231. ["<C-f>"] = cmp.mapping.scroll_docs(4),
  232. -- TODO: potentially fix emmet nonsense
  233. ["<Tab>"] = cmp.mapping(function(fallback)
  234. if cmp.visible() then
  235. cmp.select_next_item()
  236. elseif luasnip.expandable() then
  237. luasnip.expand()
  238. elseif jumpable() then
  239. luasnip.jump(1)
  240. elseif check_backspace() then
  241. fallback()
  242. elseif is_emmet_active() then
  243. return vim.fn["cmp#complete"]()
  244. else
  245. fallback()
  246. end
  247. end, {
  248. "i",
  249. "s",
  250. }),
  251. ["<S-Tab>"] = cmp.mapping(function(fallback)
  252. if cmp.visible() then
  253. cmp.select_prev_item()
  254. elseif jumpable(-1) then
  255. luasnip.jump(-1)
  256. else
  257. fallback()
  258. end
  259. end, {
  260. "i",
  261. "s",
  262. }),
  263. ["<C-Space>"] = cmp.mapping.complete(),
  264. ["<C-e>"] = cmp.mapping.abort(),
  265. ["<CR>"] = cmp.mapping(function(fallback)
  266. if cmp.visible() and cmp.confirm(lvim.builtin.cmp.confirm_opts) then
  267. if jumpable() then
  268. luasnip.jump(1)
  269. end
  270. return
  271. end
  272. if jumpable() then
  273. if not luasnip.jump(1) then
  274. fallback()
  275. end
  276. else
  277. fallback()
  278. end
  279. end),
  280. },
  281. }
  282. end
  283. function M.setup()
  284. require("cmp").setup(lvim.builtin.cmp)
  285. end
  286. return M