-- language_spec = { treesitter?, lsp?, linter?, formatter?, filetype? } local language_specs = { -- Docs / Config 'vim', 'vimdoc', { 'markdown', nil, nil, 'prettier' }, 'markdown_inline', 'yaml', 'toml', -- Data 'gitcommit', 'gitignore', 'dockerfile', 'diff', { 'json', 'json-lsp' }, { 'jsonc', 'json-lsp' }, -- Shell / scripting { 'bash', 'bash-language-server', 'shellcheck', 'shfmt' }, { 'lua', 'lua-language-server', 'luacheck', 'stylua' }, 'sql', -- Programming 'c', 'cpp', 'go', 'rust', 'python', -- { 'python', 'pyright', 'ruff', 'ruff' }, -- install ensurepip -- Web stack { 'html', 'html-lsp' }, { 'css', 'css-lsp' }, { 'javascript', { 'vtsls', 'eslint-lsp' }, nil, 'prettier' }, { 'typescript', { 'vtsls', 'eslint-lsp' }, nil, 'prettier' }, { nil, { 'vtsls', 'eslint-lsp' }, nil, 'prettier', filetype = 'javascriptreact' }, { 'tsx', { 'vtsls', 'eslint-lsp' }, nil, 'prettier', 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', { 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 { { 'windwp/nvim-ts-autotag', config = true }, { 'windwp/nvim-autopairs', event = 'InsertEnter', config = true }, { 'mason-org/mason.nvim', config = function() local mason = require('mason') local registry = require('mason-registry') 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, }, { 'nvim-treesitter/nvim-treesitter', build = ':TSUpdate', main = 'nvim-treesitter.configs', opts = { highlight = { enable = true }, incremental_selection = { enable = true }, ensure_installed = general.ts_parsers, }, }, { 'mfussenegger/nvim-lint', event = { 'BufReadPre', 'BufNewFile' }, opts = { linters_by_ft = general.linters_by_ft }, config = function(_, opts) local lint = require('lint') lint.linters_by_ft = opts.linters_by_ft vim.api.nvim_create_autocmd({ 'BufEnter', 'BufWritePost', 'InsertLeave' }, { group = vim.api.nvim_create_augroup('lint_autocmd', { clear = true }), callback = function() lint.try_lint() end, }) end, }, { 'stevearc/conform.nvim', event = { 'BufWritePre' }, opts = { format_on_save = { timeout_ms = 500, lsp_format = 'fallback' }, default_format_opts = { stop_after_first = true }, formatters_by_ft = general.formatters_by_ft, }, }, }