diff --git a/init.lua b/init.lua index 9a08766..93b7bfc 100644 --- a/init.lua +++ b/init.lua @@ -18,7 +18,6 @@ vim.opt.rtp:prepend(lazypath) require('config.options') require('config.keymaps') require('config.autocmds') -require('config.clipboard') require('config.terminal') require('custom.navigation') require('custom.tabline').setup() diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index 94c0d34..53255d5 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -48,8 +48,30 @@ vim.api.nvim_create_autocmd({ 'WinLeave', 'InsertEnter' }, { end, }) --- Removes trailing whitespace before saving -vim.api.nvim_create_autocmd({ 'BufWritePre' }, { - pattern = '*', - command = [[%s/\s\+$//e]], +-- Autocompletion +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, }) +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 diff --git a/lua/config/clipboard.lua b/lua/config/clipboard.lua deleted file mode 100644 index 2dc7e12..0000000 --- a/lua/config/clipboard.lua +++ /dev/null @@ -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, {}) diff --git a/lua/config/diagnostics.lua b/lua/config/diagnostics.lua new file mode 100644 index 0000000..6e341ea --- /dev/null +++ b/lua/config/diagnostics.lua @@ -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, +} diff --git a/lua/config/keymaps.lua b/lua/config/keymaps.lua index 1b7f91b..837f9a4 100644 --- a/lua/config/keymaps.lua +++ b/lua/config/keymaps.lua @@ -21,7 +21,7 @@ vim.keymap.set('x', 'K', ":m '<-2gv=gv") vim.keymap.set('n', 's', [[:%s/\<\>//g]]) --- Proper registers +-- Easy to use registers map('x', 'p', '"_dP') map({ 'n', 'x' }, 'y', '"+y') map('n', 'Y', '"+y$') @@ -60,8 +60,12 @@ map('n', 'xr', cmd('TermRelative')) map('n', 'xs', cmd('TermSplit')) map('n', 'xv', cmd('TermVSplit')) map('t', '', '') -map('t', '', '') -map('t', 'c', ':bd!') +map('t', 'h', [[h]]) +map('t', 'j', [[j]]) +map('t', 'k', [[k]]) +map('t', 'l', [[l]]) +map('t', 'c', [[bd!]]) +map('t', '', [[w]]) -- File explorer map('n', 'e', cmd('NvimTreeToggle')) @@ -77,3 +81,8 @@ end) map('n', 'q', vim.diagnostic.setloclist) map('n', 'd', vim.diagnostic.open_float) map('n', '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', '', vim.lsp.buf.signature_help) diff --git a/lua/config/options.lua b/lua/config/options.lua index f625602..4c3d5ce 100644 --- a/lua/config/options.lua +++ b/lua/config/options.lua @@ -16,11 +16,15 @@ vim.g.loaded_vimballPlugin = 1 vim.g.loaded_matchit = 1 vim.g.loaded_2html_plugin = 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_netrwPlugin = 1 -- UI -vim.g.health = { style = 'float' } vim.g.have_nerd_font = true vim.opt.termguicolors = false @@ -31,9 +35,10 @@ vim.opt.signcolumn = 'no' vim.opt.number = true vim.opt.relativenumber = true vim.opt.cursorline = true +vim.opt.ruler = false vim.opt.winborder = 'rounded' vim.opt.guicursor = 'n-v-i-c:block' -vim.opt.ruler = false + vim.opt.laststatus = 3 vim.opt.statusline = '── %f %h%w%m%r %= [%l,%c-%L] ──' vim.opt.fillchars = { @@ -71,7 +76,7 @@ vim.opt.mouse = 'a' -- Enable mouse support vim.opt.ignorecase = true vim.opt.smartcase = true -- Override ignorecase if search contains upper case chars vim.opt.inccommand = 'split' -- Live substitution preview -vim.opt.completeopt = { 'fuzzy,menuone,popup,preview,noselect' } +vim.opt.completeopt = { 'fuzzy', 'menuone', 'popup', 'noselect' } -- Splits vim.opt.splitright = true @@ -87,3 +92,18 @@ vim.opt.timeout = true vim.opt.ttimeout = true vim.opt.timeoutlen = 500 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 diff --git a/lua/config/terminal.lua b/lua/config/terminal.lua index 6221433..675262e 100644 --- a/lua/config/terminal.lua +++ b/lua/config/terminal.lua @@ -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', { group = term_group, callback = function() @@ -10,18 +12,6 @@ vim.api.nvim_create_autocmd('TermOpen', { 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) vim.api.nvim_create_autocmd('BufEnter', { group = term_group, @@ -33,30 +23,43 @@ vim.api.nvim_create_autocmd('BufEnter', { end, }) -local function open_default() - vim.cmd('terminal') -end +-- Close all terminal buffers before quitting +vim.api.nvim_create_autocmd('QuitPre', { + 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 shell = vim.o.shell or 'zsh' - local dir = vim.fn.expand('%:p:h') - vim.cmd(string.format('edit term://%s//%s', dir, shell)) -end +local commands = { + TermDefault = function() + vim.cmd('terminal') + end, -local function open_split() - vim.cmd('new') - vim.cmd('wincmd J') - vim.api.nvim_win_set_height(0, 12) - vim.wo.winfixheight = true - vim.cmd('term') -end + TermRelative = function() + local shell = vim.o.shell or 'zsh' + local dir = vim.fn.expand('%:p:h') + vim.cmd(string.format('edit term://%s//%s', dir, shell)) + end, -local function open_vertical() - vim.cmd('vsplit') - vim.cmd('term') -end + TermSplit = function() + vim.cmd('new') + vim.cmd('wincmd J') + 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, {}) -vim.api.nvim_create_user_command('TermRelative', open_relative, {}) -vim.api.nvim_create_user_command('TermSplit', open_split, {}) -vim.api.nvim_create_user_command('TermVSplit', open_vertical, {}) + TermVSplit = function() + vim.cmd('vsplit') + vim.cmd('term') + end, +} + +for name, fn in pairs(commands) do + vim.api.nvim_create_user_command(name, fn, {}) +end diff --git a/lua/custom/language-manager.lua b/lua/custom/language-manager.lua new file mode 100644 index 0000000..48d86f0 --- /dev/null +++ b/lua/custom/language-manager.lua @@ -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 + -- -> { ft = } + 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 diff --git a/lua/custom/lm-cmds.lua b/lua/custom/lm-cmds.lua new file mode 100644 index 0000000..868cc2f --- /dev/null +++ b/lua/custom/lm-cmds.lua @@ -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' }) diff --git a/lua/plugins/syntax.lua b/lua/plugins/syntax.lua index 390ea48..3ca2f6f 100644 --- a/lua/plugins/syntax.lua +++ b/lua/plugins/syntax.lua @@ -1,344 +1,32 @@ --- language_spec = { treesitter?, lsp?, linter?, formatter?, filetype? } +local lm = require('custom.language-manager') local language_specs = { - -- Docs / Config - 'vim', - 'vimdoc', - { 'markdown', nil, nil, 'prettier' }, - 'markdown_inline', - 'yaml', - 'toml', + { ts = { 'yaml', 'toml', 'sql', 'diff', 'dockerfile', 'gitcommit', 'gitignore' } }, + { ts = { 'c', 'cpp', 'go', 'rust', 'python' } }, - -- 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', '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, + { ft = 'markdown', ts = { 'markdown', 'markdown_inline' }, format = 'prettier' }, + { ft = 'bash', lsp = 'bash-language-server', lint = 'shellcheck', format = 'shfmt' }, + { ft = 'lua', lsp = 'lua-language-server', lint = 'luacheck', format = 'stylua' }, + { ft = { 'json', 'jsonc' }, lsp = 'json-lsp' }, + { ft = 'html', lsp = 'html-lsp' }, + { ft = 'css', lsp = { 'css-lsp', 'tailwindcss-language-server' } }, + { + ft = { 'javascript', 'typescript', 'javascriptreact', 'typescriptreact' }, + ts = { 'javascript', 'typescript', 'tsx' }, + lsp = { 'vtsls', 'eslint-lsp' }, + format = { 'prettierd', 'prettier' }, }, - - 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 +local general = lm.generate_specs(language_specs) - -- 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) + require('mason').setup() + lm.lsp.enable() end, }, {