feat: language-manager

This commit is contained in:
Tomas Mirchev 2025-10-24 02:21:21 +03:00
parent 9bca643408
commit 2b1b3ebbf0
10 changed files with 385 additions and 401 deletions

View File

@ -18,7 +18,6 @@ vim.opt.rtp:prepend(lazypath)
require('config.options') require('config.options')
require('config.keymaps') require('config.keymaps')
require('config.autocmds') require('config.autocmds')
require('config.clipboard')
require('config.terminal') require('config.terminal')
require('custom.navigation') require('custom.navigation')
require('custom.tabline').setup() require('custom.tabline').setup()

View File

@ -48,8 +48,30 @@ vim.api.nvim_create_autocmd({ 'WinLeave', 'InsertEnter' }, {
end, end,
}) })
-- Removes trailing whitespace before saving -- Autocompletion
vim.api.nvim_create_autocmd({ 'BufWritePre' }, { vim.api.nvim_create_autocmd('LspAttach', {
pattern = '*', group = vim.api.nvim_create_augroup('minimal_lsp', { clear = true }),
command = [[%s/\s\+$//e]], callback = function(ev)
local client = vim.lsp.get_client_by_id(ev.data.client_id)
if not client then
return
end
-- Enable native completion
if client:supports_method('textDocument/completion') then
vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true })
end
end,
}) })
vim.lsp.handlers['textDocument/completion'] = function(err, result, ctx, config)
if err or not result then
return
end
for _, item in ipairs(result.items or result) do
if item.kind then
local kind = vim.lsp.protocol.CompletionItemKind[item.kind] or ''
item.menu = '[' .. kind .. ']'
end
end
return vim.lsp.completion._on_completion_result(err, result, ctx, config)
end

View File

@ -1,25 +0,0 @@
if vim.env.CONTAINER then
vim.g.clipboard = {
name = 'osc52',
copy = {
['+'] = require('vim.ui.clipboard.osc52').copy('+'),
['*'] = require('vim.ui.clipboard.osc52').copy('*'),
},
paste = {
['+'] = require('vim.ui.clipboard.osc52').paste('+'),
['*'] = require('vim.ui.clipboard.osc52').paste('*'),
},
}
end
-- vim.schedule(function()
-- vim.opt.clipboard = 'unnamedplus'
-- end)
-- TEMP: Check if it helps with edge cases
vim.api.nvim_create_user_command('FixClipboard', function()
vim.cmd('lua require("vim.ui.clipboard.osc52")')
vim.schedule(function()
vim.notify('Clipboard provider reloaded (OSC52)')
end)
end, {})

View File

@ -0,0 +1,42 @@
-- Diagnostics
local special_sources = {
lua_ls = 'lua',
}
vim.diagnostic.config({
underline = true,
severity_sort = true,
virtual_text = {
format = function(diagnostic)
local src = diagnostic.source and (special_sources[diagnostic.source] or diagnostic.source)
if src then
return string.format('%s: %s', src, diagnostic.message)
end
return diagnostic.message
end,
},
float = {
border = 'rounded',
header = '',
format = function(diagnostic)
local src = diagnostic.source and (special_sources[diagnostic.source] or diagnostic.source)
if src then
return string.format('%s: %s', src, diagnostic.message)
end
return diagnostic.message
end,
},
})
-- Override the virtual text diagnostic handler so that the most severe diagnostic is shown first.
local show_handler = vim.diagnostic.handlers.virtual_text.show
assert(show_handler)
local hide_handler = vim.diagnostic.handlers.virtual_text.hide
vim.diagnostic.handlers.virtual_text = {
show = function(ns, bufnr, diagnostics, opts)
table.sort(diagnostics, function(diag1, diag2)
return diag1.severity > diag2.severity
end)
return show_handler(ns, bufnr, diagnostics, opts)
end,
hide = hide_handler,
}

View File

@ -21,7 +21,7 @@ vim.keymap.set('x', 'K', ":m '<-2<CR>gv=gv")
vim.keymap.set('n', '<leader>s', [[:%s/\<<C-r><C-w>\>/<C-r><C-w>/g<Left><Left><Left>]]) vim.keymap.set('n', '<leader>s', [[:%s/\<<C-r><C-w>\>/<C-r><C-w>/g<Left><Left><Left>]])
-- Proper registers -- Easy to use registers
map('x', '<leader>p', '"_dP') map('x', '<leader>p', '"_dP')
map({ 'n', 'x' }, '<leader>y', '"+y') map({ 'n', 'x' }, '<leader>y', '"+y')
map('n', '<leader>Y', '"+y$') map('n', '<leader>Y', '"+y$')
@ -60,8 +60,12 @@ map('n', '<leader>xr', cmd('TermRelative'))
map('n', '<leader>xs', cmd('TermSplit')) map('n', '<leader>xs', cmd('TermSplit'))
map('n', '<leader>xv', cmd('TermVSplit')) map('n', '<leader>xv', cmd('TermVSplit'))
map('t', '<Esc>', '<C-\\><C-n>') map('t', '<Esc>', '<C-\\><C-n>')
map('t', '<C-w>', '<C-\\><C-n><C-w>') map('t', '<C-w>h', [[<C-\><C-n><C-w>h]])
map('t', '<C-w>c', '<C-\\><C-n>:bd!<CR>') map('t', '<C-w>j', [[<C-\><C-n><C-w>j]])
map('t', '<C-w>k', [[<C-\><C-n><C-w>k]])
map('t', '<C-w>l', [[<C-\><C-n><C-w>l]])
map('t', '<C-w>c', [[<C-\><C-n><cmd>bd!<CR>]])
map('t', '<C-w><C-w>', [[<C-\><C-n><C-w>w]])
-- File explorer -- File explorer
map('n', '<leader>e', cmd('NvimTreeToggle')) map('n', '<leader>e', cmd('NvimTreeToggle'))
@ -77,3 +81,8 @@ end)
map('n', '<leader>q', vim.diagnostic.setloclist) map('n', '<leader>q', vim.diagnostic.setloclist)
map('n', '<leader>d', vim.diagnostic.open_float) map('n', '<leader>d', vim.diagnostic.open_float)
map('n', '<leader>s', vim.lsp.buf.signature_help) map('n', '<leader>s', vim.lsp.buf.signature_help)
map('n', 'gd', vim.lsp.buf.definition)
map('n', 'gr', vim.lsp.buf.references)
map('n', 'K', vim.lsp.buf.hover)
map('n', '<C-s>', vim.lsp.buf.signature_help)

View File

@ -16,11 +16,15 @@ vim.g.loaded_vimballPlugin = 1
vim.g.loaded_matchit = 1 vim.g.loaded_matchit = 1
vim.g.loaded_2html_plugin = 1 vim.g.loaded_2html_plugin = 1
vim.g.loaded_rrhelper = 1 vim.g.loaded_rrhelper = 1
vim.g.loaded_matchparen = 1
vim.g.loaded_tutor_mode_plugin = 1
vim.g.loaded_spellfile_plugin = 1
vim.g.loaded_logipat = 1
vim.g.loaded_rplugin = 1
vim.g.loaded_netrw = 1 -- use nvim-tree instead vim.g.loaded_netrw = 1 -- use nvim-tree instead
vim.g.loaded_netrwPlugin = 1 vim.g.loaded_netrwPlugin = 1
-- UI -- UI
vim.g.health = { style = 'float' }
vim.g.have_nerd_font = true vim.g.have_nerd_font = true
vim.opt.termguicolors = false vim.opt.termguicolors = false
@ -31,9 +35,10 @@ vim.opt.signcolumn = 'no'
vim.opt.number = true vim.opt.number = true
vim.opt.relativenumber = true vim.opt.relativenumber = true
vim.opt.cursorline = true vim.opt.cursorline = true
vim.opt.ruler = false
vim.opt.winborder = 'rounded' vim.opt.winborder = 'rounded'
vim.opt.guicursor = 'n-v-i-c:block' vim.opt.guicursor = 'n-v-i-c:block'
vim.opt.ruler = false
vim.opt.laststatus = 3 vim.opt.laststatus = 3
vim.opt.statusline = '── %f %h%w%m%r %= [%l,%c-%L] ──' vim.opt.statusline = '── %f %h%w%m%r %= [%l,%c-%L] ──'
vim.opt.fillchars = { vim.opt.fillchars = {
@ -71,7 +76,7 @@ vim.opt.mouse = 'a' -- Enable mouse support
vim.opt.ignorecase = true vim.opt.ignorecase = true
vim.opt.smartcase = true -- Override ignorecase if search contains upper case chars vim.opt.smartcase = true -- Override ignorecase if search contains upper case chars
vim.opt.inccommand = 'split' -- Live substitution preview vim.opt.inccommand = 'split' -- Live substitution preview
vim.opt.completeopt = { 'fuzzy,menuone,popup,preview,noselect' } vim.opt.completeopt = { 'fuzzy', 'menuone', 'popup', 'noselect' }
-- Splits -- Splits
vim.opt.splitright = true vim.opt.splitright = true
@ -87,3 +92,18 @@ vim.opt.timeout = true
vim.opt.ttimeout = true vim.opt.ttimeout = true
vim.opt.timeoutlen = 500 vim.opt.timeoutlen = 500
vim.opt.ttimeoutlen = 10 vim.opt.ttimeoutlen = 10
-- Clipboard
if vim.env.CONTAINER then
vim.g.clipboard = {
name = 'osc52',
copy = {
['+'] = require('vim.ui.clipboard.osc52').copy('+'),
['*'] = require('vim.ui.clipboard.osc52').copy('*'),
},
paste = {
['+'] = require('vim.ui.clipboard.osc52').paste('+'),
['*'] = require('vim.ui.clipboard.osc52').paste('*'),
},
}
end

View File

@ -1,4 +1,6 @@
local term_group = vim.api.nvim_create_augroup('custom-term-open', { clear = true }) local term_group = vim.api.nvim_create_augroup('custom-term', { clear = true })
-- Custom terminal
vim.api.nvim_create_autocmd('TermOpen', { vim.api.nvim_create_autocmd('TermOpen', {
group = term_group, group = term_group,
callback = function() callback = function()
@ -10,18 +12,6 @@ vim.api.nvim_create_autocmd('TermOpen', {
end, end,
}) })
-- Close all terminal buffers before quitting
vim.api.nvim_create_autocmd('QuitPre', {
group = vim.api.nvim_create_augroup('shoutoff_terminals', { clear = true }),
callback = function()
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].buftype == 'terminal' then
vim.api.nvim_buf_delete(buf, { force = true })
end
end
end,
})
-- Insert when re-entering a terminal window (after switching back) -- Insert when re-entering a terminal window (after switching back)
vim.api.nvim_create_autocmd('BufEnter', { vim.api.nvim_create_autocmd('BufEnter', {
group = term_group, group = term_group,
@ -33,30 +23,43 @@ vim.api.nvim_create_autocmd('BufEnter', {
end, end,
}) })
local function open_default() -- Close all terminal buffers before quitting
vim.cmd('terminal') vim.api.nvim_create_autocmd('QuitPre', {
end group = term_group,
callback = function()
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].buftype == 'terminal' then
vim.api.nvim_buf_delete(buf, { force = true })
end
end
end,
})
local function open_relative() local commands = {
local shell = vim.o.shell or 'zsh' TermDefault = function()
local dir = vim.fn.expand('%:p:h') vim.cmd('terminal')
vim.cmd(string.format('edit term://%s//%s', dir, shell)) end,
end
local function open_split() TermRelative = function()
vim.cmd('new') local shell = vim.o.shell or 'zsh'
vim.cmd('wincmd J') local dir = vim.fn.expand('%:p:h')
vim.api.nvim_win_set_height(0, 12) vim.cmd(string.format('edit term://%s//%s', dir, shell))
vim.wo.winfixheight = true end,
vim.cmd('term')
end
local function open_vertical() TermSplit = function()
vim.cmd('vsplit') vim.cmd('new')
vim.cmd('term') vim.cmd('wincmd J')
end vim.api.nvim_win_set_height(0, 12)
vim.wo.winfixheight = true
vim.cmd('term')
end,
vim.api.nvim_create_user_command('TermDefault', open_default, {}) TermVSplit = function()
vim.api.nvim_create_user_command('TermRelative', open_relative, {}) vim.cmd('vsplit')
vim.api.nvim_create_user_command('TermSplit', open_split, {}) vim.cmd('term')
vim.api.nvim_create_user_command('TermVSplit', open_vertical, {}) end,
}
for name, fn in pairs(commands) do
vim.api.nvim_create_user_command(name, fn, {})
end

View File

@ -0,0 +1,128 @@
--[[
local lm = require('language_manager')
lm.install() or .install({'ts_parsers','language_servers','linters','formatters'})
- treesitter.install specs.ts_parsers
- mason.install mason-registry.get_package(name):install()
lm.generate_specs() or {use_cache=true, only_installed=true}
lm.delete_cache()
lm.enable()
--]]
local M = {}
local function wrap(item)
if type(item) == 'string' then
return { item }
elseif type(item) == 'table' then
return item
else
return {}
end
end
local function create_set()
local seen = {}
return function(ref, item) -- unique()
if not seen[ref] then
seen[ref] = {}
end
if item and not seen[ref][item] then
seen[ref][item] = true
return true
end
return false
end
end
local function create_spec()
local M = {}
local unique = create_set()
local specs = {}
function M.set(item, group)
specs[group] = item
end
function M.get()
return specs
end
function M.add(list, ...)
local groups = { ... }
local ref = table.concat(groups, '.')
local pointer = specs
local last_i = #groups
for i = 1, last_i do
local group = groups[i]
if i == last_i then
for _, item in ipairs(wrap(list)) do
if unique(ref, item) then
if not pointer[group] then
pointer[group] = {}
end
table.insert(pointer[group], item)
end
end
else
if not pointer[group] then
pointer[group] = {}
end
pointer = pointer[group]
end
end
end
return M
end
function M.generate_specs(specs_raw)
local specs = create_spec()
for _, spec in ipairs(specs_raw) do
-- <filetype> -> { ft = <filetype> }
if type(spec) == 'string' then
spec = { ft = spec }
end
-- Filetype = TS Parser
if not spec.ts then
spec.ts = spec.ft
end
specs.add(spec.ts, 'ts_parsers')
specs.add(spec.lsp, 'language_servers')
for _, filetype in ipairs(wrap(spec.ft)) do
specs.add(spec.lint, 'linters_by_ft', filetype)
specs.add(spec.format, 'formatters_by_ft', filetype)
end
end
M.general = specs.get()
return M.general
end
M.lsp = {}
function M.lsp.enable()
local registry = require('mason-registry')
local lsp_configs = {}
for _, lsp in ipairs(M.general.language_servers) do
if registry.has_package(lsp) then
local pkg = registry.get_package(lsp)
local spec = pkg.spec and pkg.spec.neovim
local lsp_name = (spec and spec.lspconfig) or lsp
table.insert(lsp_configs, lsp_name)
vim.lsp.enable(lsp_name)
else
vim.notify('Unknown LSP: ' .. lsp, vim.log.levels.WARN)
end
end
vim.notify('Enabled LSPs: ' .. table.concat(lsp_configs, ', '), vim.log.levels.INFO)
end
return M

98
lua/custom/lm-cmds.lua Normal file
View File

@ -0,0 +1,98 @@
local general = {}
---------------------------------------------------------------------
-- Mason Install Command
---------------------------------------------------------------------
vim.api.nvim_create_user_command('MasonInstallAll', function()
local registry = require('mason-registry')
local list = vim.list_extend(vim.list_extend({}, general.lsps), {})
for _, ftmap in pairs({ general.linters_by_ft, general.formatters_by_ft }) do
for _, tools in pairs(ftmap) do
vim.list_extend(list, tools)
end
end
list = uniq(list)
local installed = {}
for _, pkg in ipairs(registry.get_installed_packages()) do
installed[pkg.name] = true
end
for _, name in ipairs(list) do
if registry.has_package(name) then
if not installed[name] then
vim.notify('Installing ' .. name, vim.log.levels.INFO)
registry.get_package(name):install()
else
vim.notify('Already installed ' .. name, vim.log.levels.INFO)
end
else
vim.notify('Package not found in registry: ' .. name, vim.log.levels.WARN)
end
end
end, { desc = 'Install all Mason LSPs, linters, and formatters' })
---------------------------------------------------------------------
-- Fetch LSP default configs from nvim-lspconfig
---------------------------------------------------------------------
vim.api.nvim_create_user_command('FetchLspConfigs', function()
local registry = require('mason-registry')
local lspconfig_names = {}
for _, lsp in ipairs(general.lsps) do
if registry.has_package(lsp) then
local pkg = registry.get_package(lsp)
local spec = pkg.spec and pkg.spec.neovim
if spec and spec.lspconfig then
table.insert(lspconfig_names, spec.lspconfig)
else
table.insert(lspconfig_names, lsp)
end
else
table.insert(lspconfig_names, lsp)
end
end
lspconfig_names = uniq(lspconfig_names)
-- base URL same as your original
local base_url = 'https://raw.githubusercontent.com/neovim/nvim-lspconfig/master/lsp/'
-- write to current working directory
local lsp_dir = vim.fs.joinpath(vim.fn.getcwd(), 'lsp')
vim.fn.mkdir(lsp_dir, 'p')
for _, name in ipairs(lspconfig_names) do
local file = vim.fs.joinpath(lsp_dir, name .. '.lua')
if vim.fn.filereadable(file) == 0 then
local url = base_url .. name .. '.lua'
local cmd = string.format('curl -fsSL -o %q %q', file, url)
vim.fn.system(cmd)
if vim.v.shell_error ~= 0 then
vim.notify('Failed to fetch ' .. name .. '.lua', vim.log.levels.ERROR)
vim.fn.delete(file)
else
vim.notify('Fetched ' .. name .. '.lua', vim.log.levels.INFO)
end
else
vim.notify('Skipped existing ' .. name .. '.lua', vim.log.levels.INFO)
end
end
end, { desc = 'Fetch default LSP configs into ./lsp in cwd' })
vim.api.nvim_create_user_command('TreesitterInstallAll', function()
local parsers = require('nvim-treesitter.parsers')
local configs = require('nvim-treesitter.configs')
local langs = configs.get_module('ensure_installed') or {}
if type(langs) == 'string' and langs == 'all' then
vim.notify('Treesitter ensure_installed = "all" not supported here', vim.log.levels.WARN)
return
end
for _, lang in ipairs(langs) do
if not parsers.has_parser(lang) then
vim.cmd('TSInstall ' .. lang)
else
vim.notify('Parser already installed: ' .. lang, vim.log.levels.INFO)
end
end
end, { desc = 'Install all Treesitter parsers defined in ensure_installed' })

View File

@ -1,344 +1,32 @@
-- language_spec = { treesitter?, lsp?, linter?, formatter?, filetype? } local lm = require('custom.language-manager')
local language_specs = { local language_specs = {
-- Docs / Config { ts = { 'yaml', 'toml', 'sql', 'diff', 'dockerfile', 'gitcommit', 'gitignore' } },
'vim', { ts = { 'c', 'cpp', 'go', 'rust', 'python' } },
'vimdoc',
{ 'markdown', nil, nil, 'prettier' },
'markdown_inline',
'yaml',
'toml',
-- Data { ft = 'markdown', ts = { 'markdown', 'markdown_inline' }, format = 'prettier' },
'gitcommit', { ft = 'bash', lsp = 'bash-language-server', lint = 'shellcheck', format = 'shfmt' },
'gitignore', { ft = 'lua', lsp = 'lua-language-server', lint = 'luacheck', format = 'stylua' },
'dockerfile', { ft = { 'json', 'jsonc' }, lsp = 'json-lsp' },
'diff', { ft = 'html', lsp = 'html-lsp' },
{ 'json', 'json-lsp' }, { ft = 'css', lsp = { 'css-lsp', 'tailwindcss-language-server' } },
{ 'jsonc', 'json-lsp' }, {
ft = { 'javascript', 'typescript', 'javascriptreact', 'typescriptreact' },
-- Shell / scripting ts = { 'javascript', 'typescript', 'tsx' },
{ 'bash', 'bash-language-server', 'shellcheck', 'shfmt' }, lsp = { 'vtsls', 'eslint-lsp' },
{ 'lua', 'lua-language-server', 'luacheck', 'stylua' }, format = { 'prettierd', 'prettier' },
'sql',
-- Programming
'c',
'cpp',
'go',
'rust',
'python',
-- { 'python', 'pyright', 'ruff', 'ruff' }, -- install ensurepip
-- Web stack
{ 'html', 'html-lsp' },
{ 'css', { 'css-lsp', 'tailwindcss-language-server' } },
{ 'javascript', { 'vtsls', 'eslint-lsp' }, nil, 'prettierd' },
{ 'typescript', { 'vtsls', 'eslint-lsp' }, nil, 'prettierd' },
{ nil, { 'vtsls', 'eslint-lsp' }, nil, 'prettierd', filetype = 'javascriptreact' },
{ 'tsx', { 'vtsls', 'eslint-lsp' }, nil, 'prettierd', filetype = 'typescriptreact' },
}
local function normalize(spec)
if type(spec) == 'string' then
spec = { spec }
end
return {
treesitter = spec[1],
lsp = spec[2],
linter = spec[3],
formatter = spec[4],
filetype = spec.filetype or spec[1],
}
end
local normalized = vim.tbl_map(normalize, language_specs)
local function uniq(tbl)
local seen, out = {}, {}
for _, v in ipairs(tbl) do
if v and not seen[v] then
seen[v] = true
table.insert(out, v)
end
end
return out
end
local function collect(specs)
local ts, lsps, linters, formatters = {}, {}, {}, {}
for _, s in ipairs(specs) do
if s.treesitter then
table.insert(ts, s.treesitter)
end
if s.lsp then
if type(s.lsp) == 'table' then
for _, v in ipairs(s.lsp) do
table.insert(lsps, v)
end
else
table.insert(lsps, s.lsp)
end
end
if s.linter then
linters[s.filetype] = { s.linter }
end
if s.formatter then
formatters[s.filetype] = { s.formatter }
end
end
return {
ts_parsers = uniq(ts),
lsps = uniq(lsps),
linters_by_ft = linters,
formatters_by_ft = formatters,
}
end
local general = collect(normalized)
---------------------------------------------------------------------
-- Mason Install Command
---------------------------------------------------------------------
vim.api.nvim_create_user_command('MasonInstallAll', function()
local registry = require('mason-registry')
local list = vim.list_extend(vim.list_extend({}, general.lsps), {})
for _, ftmap in pairs({ general.linters_by_ft, general.formatters_by_ft }) do
for _, tools in pairs(ftmap) do
vim.list_extend(list, tools)
end
end
list = uniq(list)
local installed = {}
for _, pkg in ipairs(registry.get_installed_packages()) do
installed[pkg.name] = true
end
for _, name in ipairs(list) do
if registry.has_package(name) then
if not installed[name] then
vim.notify('Installing ' .. name, vim.log.levels.INFO)
registry.get_package(name):install()
else
vim.notify('Already installed ' .. name, vim.log.levels.INFO)
end
else
vim.notify('Package not found in registry: ' .. name, vim.log.levels.WARN)
end
end
end, { desc = 'Install all Mason LSPs, linters, and formatters' })
---------------------------------------------------------------------
-- Fetch LSP default configs from nvim-lspconfig
---------------------------------------------------------------------
vim.api.nvim_create_user_command('FetchLspConfigs', function()
local registry = require('mason-registry')
local lspconfig_names = {}
for _, lsp in ipairs(general.lsps) do
if registry.has_package(lsp) then
local pkg = registry.get_package(lsp)
local spec = pkg.spec and pkg.spec.neovim
if spec and spec.lspconfig then
table.insert(lspconfig_names, spec.lspconfig)
else
table.insert(lspconfig_names, lsp)
end
else
table.insert(lspconfig_names, lsp)
end
end
lspconfig_names = uniq(lspconfig_names)
-- base URL same as your original
local base_url = 'https://raw.githubusercontent.com/neovim/nvim-lspconfig/master/lsp/'
-- write to current working directory
local lsp_dir = vim.fs.joinpath(vim.fn.getcwd(), 'lsp')
vim.fn.mkdir(lsp_dir, 'p')
for _, name in ipairs(lspconfig_names) do
local file = vim.fs.joinpath(lsp_dir, name .. '.lua')
if vim.fn.filereadable(file) == 0 then
local url = base_url .. name .. '.lua'
local cmd = string.format('curl -fsSL -o %q %q', file, url)
vim.fn.system(cmd)
if vim.v.shell_error ~= 0 then
vim.notify('Failed to fetch ' .. name .. '.lua', vim.log.levels.ERROR)
vim.fn.delete(file)
else
vim.notify('Fetched ' .. name .. '.lua', vim.log.levels.INFO)
end
else
vim.notify('Skipped existing ' .. name .. '.lua', vim.log.levels.INFO)
end
end
end, { desc = 'Fetch default LSP configs into ./lsp in cwd' })
vim.api.nvim_create_user_command('TreesitterInstallAll', function()
local parsers = require('nvim-treesitter.parsers')
local configs = require('nvim-treesitter.configs')
local langs = configs.get_module('ensure_installed') or {}
if type(langs) == 'string' and langs == 'all' then
vim.notify('Treesitter ensure_installed = "all" not supported here', vim.log.levels.WARN)
return
end
for _, lang in ipairs(langs) do
if not parsers.has_parser(lang) then
vim.cmd('TSInstall ' .. lang)
else
vim.notify('Parser already installed: ' .. lang, vim.log.levels.INFO)
end
end
end, { desc = 'Install all Treesitter parsers defined in ensure_installed' })
vim.api.nvim_create_user_command('General', function()
print(vim.inspect(general))
end, {})
local special_sources = {
lua_ls = 'lua',
eslint = 'eslint',
}
vim.diagnostic.config({
underline = true,
severity_sort = true,
update_in_insert = true,
virtual_text = {
format = function(diagnostic)
local src = diagnostic.source and (special_sources[diagnostic.source] or diagnostic.source)
if src then
return string.format('%s: %s', src, diagnostic.message)
end
return diagnostic.message
end,
}, },
float = {
border = 'rounded',
header = '',
format = function(diagnostic)
local src = diagnostic.source and (special_sources[diagnostic.source] or diagnostic.source)
if src then
return string.format('%s: %s', src, diagnostic.message)
end
return diagnostic.message
end,
},
})
-- Override the virtual text diagnostic handler so that the most severe diagnostic is shown first.
local show_handler = vim.diagnostic.handlers.virtual_text.show
assert(show_handler)
local hide_handler = vim.diagnostic.handlers.virtual_text.hide
vim.diagnostic.handlers.virtual_text = {
show = function(ns, bufnr, diagnostics, opts)
table.sort(diagnostics, function(diag1, diag2)
return diag1.severity > diag2.severity
end)
return show_handler(ns, bufnr, diagnostics, opts)
end,
hide = hide_handler,
} }
vim.api.nvim_create_autocmd('LspAttach', { local general = lm.generate_specs(language_specs)
group = vim.api.nvim_create_augroup('minimal_lsp', { clear = true }),
callback = function(ev)
local client = vim.lsp.get_client_by_id(ev.data.client_id)
if not client then
return
end
-- Enable native completion
if client:supports_method('textDocument/completion') then
vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true })
end
end,
})
---------------------------------------------------------------------
-- Optional: annotate completion items with their kind
---------------------------------------------------------------------
vim.lsp.handlers['textDocument/completion'] = function(err, result, ctx, config)
if err or not result then
return
end
for _, item in ipairs(result.items or result) do
if item.kind then
local kind = vim.lsp.protocol.CompletionItemKind[item.kind] or ''
item.menu = '[' .. kind .. ']'
end
end
return vim.lsp.completion._on_completion_result(err, result, ctx, config)
end
---------------------------------------------------------------------
-- Plugins
---------------------------------------------------------------------
vim.api.nvim_create_autocmd('LspAttach', {
group = vim.api.nvim_create_augroup('lsp_attach_timing', { clear = true }),
callback = function(ev)
local client = vim.lsp.get_client_by_id(ev.data.client_id)
if client then
vim.notify(
string.format('LSP attached: %s (buf: %d)', client.name, ev.buf),
vim.log.levels.INFO
)
end
end,
})
local function enable_lsp_with_timing(lsp_name)
local start_time = vim.uv.hrtime()
vim.lsp.enable(lsp_name)
-- Track when the server actually attaches
local group = vim.api.nvim_create_augroup('lsp_timing_' .. lsp_name, { clear = true })
vim.api.nvim_create_autocmd('LspAttach', {
group = group,
callback = function(ev)
local client = vim.lsp.get_client_by_id(ev.data.client_id)
if client and client.name == lsp_name then
local elapsed = (vim.uv.hrtime() - start_time) / 1e6 -- Convert to milliseconds
vim.notify(string.format('%s attached in %.2f ms', lsp_name, elapsed), vim.log.levels.INFO)
vim.api.nvim_del_augroup_by_id(group)
end
end,
})
end
return { return {
{ 'windwp/nvim-ts-autotag', config = true }, { 'windwp/nvim-ts-autotag', config = true },
{ 'windwp/nvim-autopairs', event = 'InsertEnter', config = true }, { 'windwp/nvim-autopairs', event = 'InsertEnter', config = true },
{ {
'mason-org/mason.nvim', 'mason-org/mason.nvim',
config = function() config = function()
local mason = require('mason') require('mason').setup()
local registry = require('mason-registry') lm.lsp.enable()
mason.setup()
local lsp_configs = {}
for _, lsp in ipairs(general.lsps) do
if registry.has_package(lsp) then
local pkg = registry.get_package(lsp)
local spec = pkg.spec and pkg.spec.neovim
local lsp_name = (spec and spec.lspconfig) or lsp
table.insert(lsp_configs, lsp_name)
-- Native enable call (Neovim ≥ 0.11)
-- vim.lsp.enable(lsp_name)
enable_lsp_with_timing(lsp_name)
else
vim.notify('Unknown LSP: ' .. lsp, vim.log.levels.WARN)
end
end
vim.notify('Enabled LSPs: ' .. table.concat(lsp_configs, ', '), vim.log.levels.INFO)
end, end,
}, },
{ {