cmp.lua 8.4 KB

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