diff --git a/init.lua b/init.lua index 07bb3ffb..2e6d9d33 100644 --- a/init.lua +++ b/init.lua @@ -1,108 +1,92 @@ +-- ~/.config/nvim/init.lua require 'core.options' -- Load general options require 'core.keymaps' -- Load general keymaps require 'core.snippets' -- Custom code snippets --- Install package manager -local lazypath = vim.fn.stdpath 'data' .. '/lazy/lazy.nvim' +-- Install package manager (lazy.nvim) +local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim' if not vim.loop.fs_stat(lazypath) then - vim.fn.system { - 'git', - 'clone', - '--filter=blob:none', + vim.fn.system({ + 'git', 'clone', '--filter=blob:none', 'https://github.com/folke/lazy.nvim.git', - '--branch=stable', -- latest stable release - lazypath, - } + '--branch=stable', lazypath, + }) end vim.opt.rtp:prepend(lazypath) + +-- Filetypes vim.filetype.add({ - extension = { - templ = "templ", - } + extension = { templ = 'templ' }, }) --- Import color theme based on environment variable NVIM_THEME +-- Theme selection (robust against unknown NVIM_THEME) local default_color_scheme = 'quantum' -local env_var_nvim_theme = os.getenv 'NVIM_THEME' or default_color_scheme - --- Define a table of theme modules +local env_var_nvim_theme = os.getenv('NVIM_THEME') or default_color_scheme local themes = { quantum = 'plugins.themes.quantum', - nord = 'plugins.themes.nord', + nord = 'plugins.themes.nord', onedark = 'plugins.themes.onedark', } +local theme_module = themes[env_var_nvim_theme] or themes[default_color_scheme] --- Setup plugins +-- Plugins require('lazy').setup({ - require(themes[env_var_nvim_theme]), + require(theme_module), require 'core.ui', - require 'plugins.aaa', -- Mason setup - require 'plugins.aerial', - require 'plugins.flash', - require 'plugins.autocompletion', - require 'plugins.bufferline', - require 'plugins.comment', - require 'plugins.conform', - require 'plugins.database', - require 'plugins.debug', - require 'plugins.gitsigns', - require 'plugins.harpoon', - require 'plugins.lazygit', - require 'plugins.lsp', + -- Load mason early so tools are ready for LSP configs + require 'plugins.mason', + + -- Core dev UX + require 'plugins.treesitter', + require 'plugins.telescope', require 'plugins.lualine', - require 'plugins.none-ls', + require 'plugins.bufferline', require 'plugins.indent-blankline', require 'plugins.neo-tree', - require 'plugins.misc', - require 'plugins.snack', - require 'plugins.telescope', require 'plugins.toggleterm', - require 'plugins.treesitter', require 'plugins.vim-tmux-navigator', require 'plugins.zellij', + require 'plugins.flash', + require 'plugins.comment', + require 'plugins.harpoon', + require 'plugins.gitsigns', + require 'plugins.lazygit', + require 'plugins.aerial', + require 'plugins.misc', + -- LSP & companions + require 'plugins.autocompletion', + require 'plugins.lsp', + require 'plugins.none-ls', -- none-ls/null-ls sources & setup + require 'plugins.autoformat', -- your autoformat-on-save/idle logic + + -- Optional: pick one formatter stack. If you keep Conform, + -- ensure it doesn't also format Go on save to avoid double-format. + require 'plugins.conform', + + -- Debugging / DB (as you had) + require 'plugins.debug', + require 'plugins.database', }, { ui = { - -- If you have a Nerd Font, set icons to an empty table which will use the - -- default lazy.nvim defined Nerd Font icons otherwise define a unicode icons table icons = vim.g.have_nerd_font and {} or { - cmd = 'โŒ˜', - config = '๐Ÿ› ', - event = '๐Ÿ“…', - ft = '๐Ÿ“‚', - init = 'โš™', - keys = '๐Ÿ—', - plugin = '๐Ÿ”Œ', - runtime = '๐Ÿ’ป', - require = '๐ŸŒ™', - source = '๐Ÿ“„', - start = '๐Ÿš€', - task = '๐Ÿ“Œ', - lazy = '๐Ÿ’ค ', + cmd = 'โŒ˜', config = '๐Ÿ› ', event = '๐Ÿ“…', ft = '๐Ÿ“‚', init = 'โš™', + keys = '๐Ÿ—', plugin = '๐Ÿ”Œ', runtime = '๐Ÿ’ป', require = '๐ŸŒ™', + source = '๐Ÿ“„', start = '๐Ÿš€', task = '๐Ÿ“Œ', lazy = '๐Ÿ’ค ', }, }, }) --- Function to check if a file exists +-- (Optional) tiny helper if you ever want to source a session file local function file_exists(file) local f = io.open(file, 'r') - if f then - f:close() - return true - else - return false - end + if f then f:close(); return true end + return false end --- Path to the session file -local session_file = '.session.vim' +-- local session_file = '.session.vim' +-- if file_exists(session_file) then vim.cmd('source ' .. session_file) end --- Check if the session file exists in the current directory --- if file_exists(session_file) then --- -- Source the session file --- vim.cmd('source ' .. session_file) --- end - --- The line beneath this is called `modeline`. See `:help modeline` -- vim: ts=2 sts=2 sw=2 et + diff --git a/lua/plugins/aaa.lua b/lua/plugins/aaa.lua deleted file mode 100644 index 916e76a2..00000000 --- a/lua/plugins/aaa.lua +++ /dev/null @@ -1,19 +0,0 @@ -return { - { - "mason-org/mason.nvim", - version = "^1.0.0", - config = function() - require("mason").setup() - end, - }, - { - "mason-org/mason-lspconfig.nvim", - version = "^1.0.0", - config = function() - require("mason-lspconfig").setup({ - ensure_installed = { "ts_ls", "gopls", "templ" }, - automatic_installation = true, - }) - end, - }, -} diff --git a/lua/plugins/autoformat.lua b/lua/plugins/autoformat.lua new file mode 100644 index 00000000..552c50d4 --- /dev/null +++ b/lua/plugins/autoformat.lua @@ -0,0 +1,72 @@ +-- lua/plugins/autoformat.lua +-- Automatically format Go code on save and when idle after changes + +return { + "neovim/nvim-lspconfig", + event = { "BufReadPre", "BufNewFile" }, + config = function() + --------------------------------------------------------------------------- + -- ๐Ÿงน Format on save + --------------------------------------------------------------------------- + vim.api.nvim_create_autocmd("BufWritePre", { + pattern = "*.go", + callback = function() + -- Runs both gopls and none-ls formatters in order + vim.lsp.buf.format({ async = false }) + end, + }) + + --------------------------------------------------------------------------- + -- โšก Auto-format when idle (after you stop typing) + --------------------------------------------------------------------------- + local format_timer = vim.loop.new_timer() + + vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { + pattern = "*.go", + callback = function() + -- Cancel previous pending format + format_timer:stop() + + -- Wait 1.5 seconds after the last change before formatting + format_timer:start(1500, 0, vim.schedule_wrap(function() + -- Only format if the buffer still exists and is listed + local bufnr = vim.api.nvim_get_current_buf() + if vim.api.nvim_buf_is_valid(bufnr) and vim.bo[bufnr].modifiable then + vim.lsp.buf.format({ async = true }) + end + end)) + end, + }) + + --------------------------------------------------------------------------- + -- ๐Ÿงช Optional: run `goimports` and quick test on save + --------------------------------------------------------------------------- + vim.api.nvim_create_autocmd("BufWritePost", { + pattern = "*.go", + callback = function() + -- Automatically fix imports using goimports if available + vim.fn.jobstart({ "goimports", "-w", vim.fn.expand("%:p") }, { + on_exit = function() + -- Optionally, trigger a quick test run for feedback + vim.fn.jobstart({ "go", "test", "./..." }, { + cwd = vim.fn.getcwd(), + stdout_buffered = true, + stderr_buffered = true, + on_stdout = function(_, data) + if data then + vim.notify(table.concat(data, "\n"), vim.log.levels.INFO, { title = "go test" }) + end + end, + on_stderr = function(_, data) + if data then + vim.notify(table.concat(data, "\n"), vim.log.levels.ERROR, { title = "go test" }) + end + end, + }) + end, + }) + end, + }) + end, +} + diff --git a/lua/plugins/lsp.lua b/lua/plugins/lsp.lua index 7ca07c8d..016b536e 100644 --- a/lua/plugins/lsp.lua +++ b/lua/plugins/lsp.lua @@ -2,27 +2,27 @@ return { { "neovim/nvim-lspconfig", + lazy = false, config = function() local lspconfig = require("lspconfig") local util = require("lspconfig.util") + local configs = require("lspconfig.configs") - -- Ensure *.templ is recognized as 'templ' - vim.filetype.add({ - extension = { templ = "templ" }, - }) + -- Recognize .templ files + vim.filetype.add({ extension = { templ = "templ" } }) - -- Go LSP with import organization + -- ============================== + -- gopls + -- ============================== lspconfig.gopls.setup({ root_dir = function(fname) return util.root_pattern("go.work", "go.mod", ".git")(fname) or util.path.dirname(fname) end, handlers = { - -- Suppress signature help errors that are common with incomplete Go code ["textDocument/signatureHelp"] = function(err, result, ctx, config) - if err and string.find(err.message, "cannot get type") then - -- Silently ignore "cannot get type" errors for signature help - return nil + if err and err.message and err.message:find("cannot get type") then + return end return vim.lsp.handlers["textDocument/signatureHelp"](err, result, ctx, config) end, @@ -59,114 +59,121 @@ return { usePlaceholders = true, completeUnimported = true, staticcheck = true, - directoryFilters = { "-.git", "-.vscode", "-.idea", "-.vscode-test", "-node_modules", "-dist", "-build", "-out", "-coverage", "-tmp", "-.cache" }, + directoryFilters = { + "-.git","-.vscode","-.idea","-.vscode-test","-node_modules", + "-dist","-build","-out","-coverage","-tmp","-.cache", + }, semanticTokens = true, - -- Performance optimizations for large repositories memoryMode = "DegradeClosed", symbolMatcher = "FastFuzzy", - -- Reduce signature help noise ["ui.completion.experimentalPostfixCompletions"] = false, }, }, }) - -- TypeScript (make sure you don't also set this up elsewhere to avoid duplicates) - lspconfig.ts_ls.setup({}) + -- ============================== + -- TypeScript / JavaScript (ts_ls OR tsserver fallback) + -- ============================== + local ts_server = lspconfig.ts_ls or lspconfig.tsserver + if ts_server then + ts_server.setup({}) + end - -- โœ… Templ LSP: auto-start when in a repo with go.mod or .git - lspconfig.templ.setup({ - cmd = { "templ", "lsp" }, -- or absolute path if needed - filetypes = { "templ" }, - root_dir = util.root_pattern("go.mod", ".git"), - single_file_support = true, - }) + -- ============================== + -- Astro (guard if missing) + -- ============================== + if lspconfig.astro then + local function get_typescript_lib() + local mason_ts = vim.fs.normalize( + "~/.local/share/nvim/mason/packages/typescript-language-server/node_modules/typescript/lib" + ) + if vim.fn.isdirectory(mason_ts) == 1 then return mason_ts end - -- LSP client monitoring helper - vim.api.nvim_create_user_command('LspClients', function() + local global_ts = (vim.fn.system("npm root -g"):gsub("\n", "")) .. "/typescript/lib" + if vim.fn.isdirectory(global_ts) == 1 then return global_ts end + + return vim.fs.normalize( + "~/.local/share/nvim/mason/packages/astro-language-server/node_modules/typescript/lib" + ) + end + + lspconfig.astro.setup({ + init_options = { typescript = { tsdk = get_typescript_lib() } }, + }) + end + + -- ============================== + -- templ (register config if missing) + -- ============================== + if not configs.templ then + configs.templ = { + default_config = { + cmd = { "templ", "lsp" }, + filetypes = { "templ" }, + root_dir = util.root_pattern("go.mod", ".git"), + single_file_support = true, + }, + } + end + lspconfig.templ.setup({}) + + -- ============================== + -- Utilities + -- ============================== + vim.api.nvim_create_user_command("LspClients", function() local clients = vim.lsp.get_clients() - local client_counts = {} - - for _, client in ipairs(clients) do - client_counts[client.name] = (client_counts[client.name] or 0) + 1 + local counts = {} + for _, c in ipairs(clients) do + counts[c.name] = (counts[c.name] or 0) + 1 end - print("=== Active LSP Clients ===") - for name, count in pairs(client_counts) do - local status = count > 1 and " โš ๏ธ DUPLICATE" or " โœ…" - print(string.format("%s: %d client(s)%s", name, count, status)) + for name, n in pairs(counts) do + local dup = n > 1 and " โš ๏ธ DUPLICATE" or " โœ…" + print(string.format("%s: %d client(s)%s", name, n, dup)) end - - if next(client_counts) == nil then - print("No active LSP clients") - end - end, { desc = "Show active LSP clients and detect duplicates" }) + if next(counts) == nil then print("No active LSP clients") end + end, {}) - -- Command to kill duplicate gopls clients (keep only the one with settings) - vim.api.nvim_create_user_command('LspKillDuplicates', function() + vim.api.nvim_create_user_command("LspKillDuplicates", function() local gopls_clients = vim.lsp.get_clients({ name = "gopls" }) if #gopls_clients <= 1 then print("No duplicate gopls clients found") return end - - local client_to_keep = nil - local clients_to_kill = {} - - -- Find the client with the most settings (should be our configured one) - for _, client in ipairs(gopls_clients) do - local settings_count = 0 - if client.config.settings and client.config.settings.gopls then - for _ in pairs(client.config.settings.gopls) do - settings_count = settings_count + 1 - end - end - - if settings_count > 0 and not client_to_keep then - client_to_keep = client - else - table.insert(clients_to_kill, client) + local keep, kill = nil, {} + for _, c in ipairs(gopls_clients) do + local cnt = 0 + if c.config.settings and c.config.settings.gopls then + for _ in pairs(c.config.settings.gopls) do cnt = cnt + 1 end end + if cnt > 0 and not keep then keep = c else table.insert(kill, c) end end - - -- Kill the duplicates - for _, client in ipairs(clients_to_kill) do - print(string.format("Killing duplicate gopls client (id: %d)", client.id)) - client.stop(true) + for _, c in ipairs(kill) do + print(("Killing duplicate gopls client (id: %d)"):format(c.id)) + c.stop(true) end - - if client_to_keep then - print(string.format("Kept gopls client (id: %d) with settings", client_to_keep.id)) - end - end, { desc = "Kill duplicate gopls clients" }) + if keep then print(("Kept gopls client (id: %d) with settings"):format(keep.id)) end + end, {}) - -- Safe hover helper local function has_hover(bufnr) for _, c in pairs(vim.lsp.get_active_clients({ bufnr = bufnr })) do - if c.server_capabilities and c.server_capabilities.hoverProvider then - return true - end + if c.server_capabilities and c.server_capabilities.hoverProvider then return true end end return false end - -- LSP keymaps are handled in lsp-keymaps.lua vim.api.nvim_create_autocmd("LspAttach", { callback = function(args) local bufnr = args.buf - - -- Use the centralized keymap system - local lsp_keymaps = require('plugins.lsp-keymaps') + local lsp_keymaps = require("plugins.lsp-keymaps") lsp_keymaps.on_attach(nil, bufnr) - - -- Safe hover (keeping this custom logic) + local function buf_map(mode, lhs, rhs, desc) vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, desc = desc }) end - + buf_map("n", "K", function() - if not has_hover(bufnr) then - return - end + if not has_hover(bufnr) then return end local ok, saga_hover = pcall(require, "lspsaga.hover") if ok and saga_hover and saga_hover.render_hover_doc then pcall(function() saga_hover:render_hover_doc() end) diff --git a/lua/plugins/mason.lua b/lua/plugins/mason.lua new file mode 100644 index 00000000..0cb8b35d --- /dev/null +++ b/lua/plugins/mason.lua @@ -0,0 +1,21 @@ + +-- lua/plugins/mason.lua +return { + { + "williamboman/mason.nvim", + lazy = false, + config = function() + require("mason").setup() + end, + }, + { + "williamboman/mason-lspconfig.nvim", + lazy = false, + dependencies = { "williamboman/mason.nvim" }, + opts = { + ensure_installed = { "gopls", "ts_ls", "templ", "astro" }, + automatic_installation = true, + automatic_setup = false, -- IMPORTANT: don't auto-setup servers + }, + }, +} diff --git a/lua/plugins/none-ls.lua b/lua/plugins/none-ls.lua index 950c3362..adea9a69 100644 --- a/lua/plugins/none-ls.lua +++ b/lua/plugins/none-ls.lua @@ -1,52 +1,38 @@ --- Format on save and linters +-- lua/plugins/none-ls.lua return { - 'nvimtools/none-ls.nvim', + "nvimtools/none-ls.nvim", + event = { "BufReadPre", "BufNewFile" }, -- load early so it can attach dependencies = { - 'nvimtools/none-ls-extras.nvim', - 'gbprod/none-ls-shellcheck.nvim', + "williamboman/mason.nvim", + "jay-babu/mason-null-ls.nvim", + "nvimtools/none-ls-extras.nvim", -- optional }, config = function() - local null_ls = require 'null-ls' - local formatting = null_ls.builtins.formatting -- to setup formatters - local diagnostics = null_ls.builtins.diagnostics -- to setup linters + local null_ls = require("null-ls") + null_ls.setup({ + sources = { + -- Go + null_ls.builtins.formatting.gofumpt, + null_ls.builtins.formatting.golines, -- optional + null_ls.builtins.diagnostics.golangci_lint, -- if installed - -- Note: Use Mason to manually install tools: - -- :MasonInstall checkmake prettier stylua eslint_d shfmt ruff goimports + -- Web (keep only what you use) + null_ls.builtins.formatting.prettierd, + null_ls.builtins.diagnostics.eslint_d, + }, + }) - local sources = { - diagnostics.checkmake, - formatting.prettier.with { filetypes = { 'html', 'json', 'yaml', 'markdown' } }, -- removed 'templ' for debugging - formatting.stylua, - formatting.shfmt.with { args = { '-i', '4' } }, - require('none-ls.formatting.ruff').with { extra_args = { '--extend-select', 'I' } }, - require 'none-ls.formatting.ruff_format', - formatting.goimports, -- Add goimports for Go files - } + require("mason-null-ls").setup({ + ensure_installed = { "gofumpt", "golines", "golangci-lint", "prettierd", "eslint_d" }, + automatic_installation = true, + }) - local augroup = vim.api.nvim_create_augroup('LspFormatting', {}) - null_ls.setup { - debug = false, -- Disable debug mode to reduce log spam - sources = sources, - -- you can reuse a shared lspconfig on_attach callback here - on_attach = function(client, bufnr) - if client.supports_method 'textDocument/formatting' then - -- Skip formatting for .templ files during debugging - local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype') - if filetype == 'templ' then - return - end - - vim.api.nvim_clear_autocmds { group = augroup, buffer = bufnr } - vim.api.nvim_create_autocmd('BufWritePre', { - group = augroup, - buffer = bufnr, - callback = function() - vim.lsp.buf.format { async = false } - end, - }) - end - end, - } + -- (optional) format on save for Go + vim.api.nvim_create_autocmd("BufWritePre", { + pattern = "*.go", + callback = function() vim.lsp.buf.format({ async = false }) end, + }) end, } + diff --git a/lua/plugins/treesitter.lua b/lua/plugins/treesitter.lua index 1b18591f..22eaf0e6 100644 --- a/lua/plugins/treesitter.lua +++ b/lua/plugins/treesitter.lua @@ -1,4 +1,4 @@ --- Highlight, edit, and navigate code + return { 'nvim-treesitter/nvim-treesitter', build = ':TSUpdate', @@ -98,7 +98,6 @@ return { }, }, } - -- Register additional file extensions vim.filetype.add { extension = { tf = 'terraform' } } vim.filetype.add { extension = { tfvars = 'terraform' } } diff --git a/lua/plugins/tscontext.lua b/lua/plugins/tscontext.lua new file mode 100644 index 00000000..b7572810 --- /dev/null +++ b/lua/plugins/tscontext.lua @@ -0,0 +1,18 @@ +return { +'treesitter-context', + setup{ + enable = true, -- Enable this plugin (Can be enabled/disabled later via commands) + multiwindow = false, -- Enable multiwindow support. + max_lines = 0, -- How many lines the window should span. Values <= 0 mean no limit. + min_window_height = 0, -- Minimum editor window height to enable context. Values <= 0 mean no limit. + line_numbers = true, + multiline_threshold = 20, -- Maximum number of lines to show for a single context + trim_scope = 'outer', -- Which context lines to discard if `max_lines` is exceeded. Choices: 'inner', 'outer' + mode = 'cursor', -- Line used to calculate context. Choices: 'cursor', 'topline' + -- Separator between context and content. Should be a single character string, like '-'. + -- When separator is set, the context will only show up when there are at least 2 lines above cursorline. + separator = nil, + zindex = 20, -- The Z-index of the context window + on_attach = nil, -- (fun(buf: integer): boolean) return false to disable attaching +} + }