388 lines
15 KiB
Lua
388 lines
15 KiB
Lua
return {
|
|
"hrsh7th/nvim-cmp",
|
|
dependencies = {
|
|
"hrsh7th/cmp-buffer",
|
|
"hrsh7th/cmp-path",
|
|
{
|
|
"L3MON4D3/LuaSnip",
|
|
-- follow latest release.
|
|
version = "v2.*", -- Replace <CurrentMajor> by the latest released major (first number of latest release)
|
|
-- install jsregexp (optional!),
|
|
build = "make install_jsregexp",
|
|
},
|
|
"saadparwaiz1/cmp_luasnip", -- autocompletion
|
|
"rafamadriz/friendly-snippets", -- snippets
|
|
"nvim-treesitter/nvim-treesitter",
|
|
"onsails/lspkind.nvim", -- vs code pictographs
|
|
"roobert/tailwindcss-colorizer-cmp.nvim",
|
|
},
|
|
config = function()
|
|
local cmp = require("cmp")
|
|
local has_luasnip, luasnip = pcall(require, 'luasnip')
|
|
local lspkind = require("lspkind")
|
|
local colorizer = require("tailwindcss-colorizer-cmp").formatter
|
|
|
|
local rhs = function(keys)
|
|
return vim.api.nvim_replace_termcodes(keys, true, true, true)
|
|
end
|
|
|
|
local lsp_kinds = {
|
|
Class = ' ',
|
|
Color = ' ',
|
|
Constant = ' ',
|
|
Constructor = ' ',
|
|
Enum = ' ',
|
|
EnumMember = ' ',
|
|
Event = ' ',
|
|
Field = ' ',
|
|
File = ' ',
|
|
Folder = ' ',
|
|
Function = ' ',
|
|
Interface = ' ',
|
|
Keyword = ' ',
|
|
Method = ' ',
|
|
Module = ' ',
|
|
Operator = ' ',
|
|
Property = ' ',
|
|
Reference = ' ',
|
|
Snippet = ' ',
|
|
Struct = ' ',
|
|
Text = ' ',
|
|
TypeParameter = ' ',
|
|
Unit = ' ',
|
|
Value = ' ',
|
|
Variable = ' ',
|
|
}
|
|
|
|
local column = function()
|
|
local _line, col = unpack(vim.api.nvim_win_get_cursor(0))
|
|
return col
|
|
end
|
|
|
|
-- luasnip custom function
|
|
local in_snippet = function()
|
|
local session = require('luasnip.session')
|
|
local node = session.current_nodes[vim.api.nvim_get_current_buf()]
|
|
if not node then
|
|
return false
|
|
end
|
|
local snippet = node.parent.snippet
|
|
local snip_begin_pos, snip_end_pos = snippet.mark:pos_begin_end()
|
|
local pos = vim.api.nvim_win_get_cursor(0)
|
|
if pos[1] - 1 >= snip_begin_pos[1] and pos[1] - 1 <= snip_end_pos[1] then
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- return true if the cursor is in leftmost column or at a whitespace char
|
|
local in_whitespace = function()
|
|
local col = column()
|
|
return col == 0 or vim.api.nvim_get_current_line():sub(col, col):match('%s')
|
|
end
|
|
|
|
local in_leading_indent = function()
|
|
local col = column()
|
|
local line = vim.api.nvim_get_current_line()
|
|
local prefix = line:sub(1, col)
|
|
return prefix:find('^%s*$')
|
|
end
|
|
|
|
-- custom shift width
|
|
local shift_width = function()
|
|
if vim.o.softtabstop <= 0 then
|
|
return vim.fn.shiftwidth()
|
|
else
|
|
return vim.o.softtabstop
|
|
end
|
|
end
|
|
|
|
-- custom smart backspace
|
|
local smart_bs = function(dedent)
|
|
local keys = nil
|
|
if vim.o.expandtab then
|
|
if dedent then
|
|
keys = rhs('<C-D>')
|
|
else
|
|
keys = rhs('<BS>')
|
|
end
|
|
else
|
|
local col = column()
|
|
local line = vim.api.nvim_get_current_line()
|
|
local prefix = line:sub(1, col)
|
|
if in_leading_indent() then
|
|
keys = rhs('<BS>')
|
|
else
|
|
local previous_char = prefix:sub(#prefix, #prefix)
|
|
if previous_char ~= ' ' then
|
|
keys = rhs('<BS>')
|
|
else
|
|
keys = rhs('<C-\\><C-o>:set expandtab<CR><BS><C-\\><C-o>:set noexpandtab<CR>')
|
|
end
|
|
end
|
|
end
|
|
vim.api.nvim_feedkeys(keys, 'nt', true)
|
|
end
|
|
|
|
-- custom smart tabs function
|
|
local smart_tab = function(opts)
|
|
local keys = nil
|
|
if vim.o.expandtab then
|
|
keys = '<Tab>' -- Neovim will insert spaces.
|
|
else
|
|
local col = column()
|
|
local line = vim.api.nvim_get_current_line()
|
|
local prefix = line:sub(1, col)
|
|
local in_leading_indent = prefix:find('^%s*$')
|
|
if in_leading_indent then
|
|
-- inserts a hard tab.
|
|
keys = '<Tab>'
|
|
else
|
|
local sw = shift_width()
|
|
local previous_char = prefix:sub(#prefix, #prefix)
|
|
local current_column = #prefix - #previous_char + 1
|
|
local remainder = (current_column - 1) % sw
|
|
local move = remainder == 0 and sw or sw - remainder
|
|
keys = (' '):rep(move)
|
|
end
|
|
end
|
|
|
|
vim.api.nvim_feedkeys(rhs(keys), 'nt', true)
|
|
end
|
|
|
|
local select_next_item = function(fallback)
|
|
if cmp.visible() then
|
|
cmp.select_next_item()
|
|
else
|
|
fallback()
|
|
end
|
|
end
|
|
|
|
local select_prev_item = function(fallback)
|
|
if cmp.visible() then
|
|
cmp.select_prev_item()
|
|
else
|
|
fallback()
|
|
end
|
|
end
|
|
|
|
local confirm = function(entry)
|
|
local behavior = cmp.ConfirmBehavior.Replace
|
|
if entry then
|
|
local completion_item = entry.completion_item
|
|
local newText = ''
|
|
if completion_item.textEdit then
|
|
newText = completion_item.textEdit.newText
|
|
elseif type(completion_item.insertText) == 'string' and completion_item.insertText ~= '' then
|
|
newText = completion_item.insertText
|
|
else
|
|
newText = completion_item.word or completion_item.label or ''
|
|
end
|
|
|
|
-- checks how many characters will be different after the cursor position if we replace,
|
|
local diff_after = math.max(0, entry.replace_range['end'].character + 1) - entry.context.cursor.col
|
|
|
|
-- does the text that will be replaced after the cursor match the suffix
|
|
-- of the newText to be inserted ? if not, then Insert instead,
|
|
if entry.context.cursor_after_line:sub(1, diff_after) ~= newText:sub(-diff_after) then
|
|
behavior = cmp.ConfirmBehavior.Insert
|
|
end
|
|
end
|
|
cmp.confirm({ select = true, behavior = behavior })
|
|
end
|
|
|
|
-- loads vscode style snippets from installed plugins (e.g. friendly-snippets)
|
|
require("luasnip.loaders.from_vscode").lazy_load()
|
|
|
|
cmp.setup({
|
|
experimental = {
|
|
-- HACK: experimenting with ghost text
|
|
-- look at `toggle_ghost_text()` function below.
|
|
ghost_text = false,
|
|
},
|
|
completion = {
|
|
completeopt = "menu,menuone,noinsert",
|
|
},
|
|
window = {
|
|
documentation = {
|
|
border = {'╭', '─', '╮', '│', '╯', '─', '╰', '│'},
|
|
},
|
|
completion = {
|
|
border = {'┌', '─', '┐', '│', '┘', '─', '└', '│'},
|
|
}
|
|
|
|
},
|
|
-- config nvim cmp to work with snippet engine
|
|
snippet = {
|
|
expand = function(args)
|
|
luasnip.lsp_expand(args.body)
|
|
end,
|
|
},
|
|
-- autocompletion sources
|
|
sources = cmp.config.sources({
|
|
{ name = "luasnip" }, -- snippets
|
|
{ name = "lazydev" },
|
|
{ name = "nvim_lsp"},
|
|
{ name = "buffer" }, -- text within current buffer
|
|
{ name = "path" }, -- file system paths
|
|
{ name = "tailwindcss-colorizer-cmp" },
|
|
{ name = "spell", -- for markdown spellchecks completions
|
|
option = {
|
|
enable_in_context = function()
|
|
local ft = vim.bo.filetype
|
|
return ft == "markdown" or ft == "text"
|
|
end,
|
|
},
|
|
},
|
|
}),
|
|
-- mapping = cmp.mapping.preset.insert({
|
|
-- ["<C-k>"] = cmp.mapping.select_prev_item(), -- previous suggestion
|
|
-- ["<C-j>"] = cmp.mapping.select_next_item(), -- next suggestion
|
|
-- ["<C-b>"] = cmp.mapping.scroll_docs(-4),
|
|
-- ["<C-f>"] = cmp.mapping.scroll_docs(4),
|
|
-- ["<C-Space>"] = cmp.mapping.complete(), -- show completion suggestions
|
|
-- ["<C-e>"] = cmp.mapping.abort(), -- close completion window
|
|
-- ["<CR>"] = cmp.mapping.confirm({ select = false }),
|
|
-- }),
|
|
|
|
-- NOTE: ! Experimenting with Customized Mappings ! --
|
|
mapping = cmp.mapping.preset.insert({
|
|
-- ['<BS>'] = cmp.mapping(function(_fallback)
|
|
-- smart_bs()
|
|
-- end, { 'i', 's' }),
|
|
|
|
["<C-e>"] = cmp.mapping.abort(), -- close completion window
|
|
['<C-d>'] = cmp.mapping(function()
|
|
cmp.close_docs()
|
|
end, { 'i', 's' }),
|
|
|
|
['<C-f>'] = cmp.mapping.scroll_docs(4),
|
|
['<C-b>'] = cmp.mapping.scroll_docs(-4),
|
|
['<C-j>'] = cmp.mapping(select_next_item),
|
|
['<C-k>'] = cmp.mapping(select_prev_item),
|
|
['<C-n>'] = cmp.mapping(select_next_item),
|
|
['<C-p>'] = cmp.mapping(select_prev_item),
|
|
['<Down>'] = cmp.mapping(select_next_item),
|
|
['<Up>'] = cmp.mapping(select_prev_item),
|
|
|
|
['<C-y>'] = cmp.mapping(function(fallback)
|
|
if cmp.visible() then
|
|
local entry = cmp.get_selected_entry()
|
|
confirm(entry)
|
|
else
|
|
fallback()
|
|
end
|
|
end, { 'i', 's' }),
|
|
|
|
['<CR>'] = cmp.mapping(function(fallback)
|
|
if cmp.visible() then
|
|
local entry = cmp.get_selected_entry()
|
|
confirm(entry)
|
|
else
|
|
fallback()
|
|
end
|
|
end, { 'i', 's' }),
|
|
|
|
['<S-Tab>'] = cmp.mapping(function(fallback)
|
|
if cmp.visible() then
|
|
cmp.select_prev_item()
|
|
elseif has_luasnip and in_snippet() and luasnip.jumpable(-1) then
|
|
luasnip.jump(-1)
|
|
elseif in_leading_indent() then
|
|
smart_bs(true) -- true means to dedent
|
|
elseif in_whitespace() then
|
|
smart_bs()
|
|
else
|
|
fallback()
|
|
end
|
|
end, { 'i', 's' }),
|
|
|
|
['<Tab>'] = cmp.mapping(function(_fallback)
|
|
if cmp.visible() then
|
|
-- if there is only one completion candidate then use it.
|
|
local entries = cmp.get_entries()
|
|
if #entries == 1 then
|
|
confirm(entries[1])
|
|
else
|
|
cmp.select_next_item()
|
|
end
|
|
elseif has_luasnip and luasnip.expand_or_locally_jumpable() then
|
|
luasnip.expand_or_jump()
|
|
elseif in_whitespace() then
|
|
smart_tab()
|
|
else
|
|
cmp.complete()
|
|
end
|
|
end, { 'i', 's' }),
|
|
}),
|
|
-- setup lspkind for vscode pictograms in autocompletion dropdown menu
|
|
formatting = {
|
|
format = function(entry, vim_item)
|
|
-- Add custom lsp_kinds icons
|
|
vim_item.kind = string.format('%s %s', lsp_kinds[vim_item.kind] or '', vim_item.kind)
|
|
|
|
|
|
-- add menu tags (e.g., [Buffer], [LSP])
|
|
vim_item.menu = ({
|
|
buffer = "[Buffer]",
|
|
nvim_lsp = "[LSP]",
|
|
luasnip = "[LuaSnip]",
|
|
nvim_lua = "[Lua]",
|
|
latex_symbols = "[LaTeX]",
|
|
})[entry.source.name]
|
|
|
|
-- use lspkind and tailwindcss-colorizer-cmp for additional formatting
|
|
vim_item = lspkind.cmp_format({
|
|
maxwidth = 25,
|
|
ellipsis_char = "...",
|
|
})(entry, vim_item)
|
|
|
|
if entry.source.name == "nvim_lsp" then
|
|
vim_item = colorizer(entry, vim_item)
|
|
end
|
|
|
|
return vim_item
|
|
end,
|
|
-- format = lspkind.cmp_format({
|
|
-- maxwidth = 30,
|
|
-- ellipsis_char = "...",
|
|
-- before = require("tailwindcss-colorizer-cmp").formatter
|
|
-- }),
|
|
-- format = require("tailwindcss-colorizer-cmp").formatter
|
|
},
|
|
})
|
|
|
|
-- NOTE: Added Ghost text stuff
|
|
-- Only show ghost text at word boundaries, not inside keywords. Based on idea
|
|
-- from: https://github.com/hrsh7th/nvim-cmp/issues/2035#issuecomment-2347186210
|
|
|
|
-- local config = require('cmp.config')
|
|
-- local toggle_ghost_text = function()
|
|
-- if vim.api.nvim_get_mode().mode ~= 'i' then
|
|
-- return
|
|
-- end
|
|
--
|
|
-- local cursor_column = vim.fn.col('.')
|
|
-- local current_line_contents = vim.fn.getline('.')
|
|
-- local character_after_cursor = current_line_contents:sub(cursor_column, cursor_column)
|
|
--
|
|
-- local should_enable_ghost_text = character_after_cursor == '' or vim.fn.match(character_after_cursor, [[\k]]) == -1
|
|
--
|
|
-- local current = config.get().experimental.ghost_text
|
|
-- if current ~= should_enable_ghost_text then
|
|
-- config.set_global({
|
|
-- experimental = {
|
|
-- ghost_text = should_enable_ghost_text,
|
|
-- },
|
|
-- })
|
|
-- end
|
|
-- end
|
|
--
|
|
-- vim.api.nvim_create_autocmd({ 'InsertEnter', 'CursorMovedI' }, {
|
|
-- callback = toggle_ghost_text,
|
|
-- })
|
|
-- ! Ghost text stuff ! --
|
|
|
|
|
|
end
|
|
}
|