diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..134c17cb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "stylua.targetReleaseVersion": "latest" +} \ No newline at end of file diff --git a/lua/core/keymaps.lua b/lua/core/keymaps.lua index 5aaca131..9d92904f 100644 --- a/lua/core/keymaps.lua +++ b/lua/core/keymaps.lua @@ -12,6 +12,9 @@ vim.g.maplocalleader = ' ' -- - Line/selection movement (Alt + jk) -- - Quick save and quit (w, W, Q) local core_keymaps = { + -- Quick access to find files by content (shortcut for sg) + { mode = 'n', lhs = '', rhs = function() require('telescope.builtin').live_grep() end, opts = { desc = 'Search text in all files' } }, + -- Clear highlights on search when pressing in normal mode { mode = 'n', lhs = '', rhs = 'nohlsearch', opts = { desc = 'Clear search highlights' } }, @@ -70,8 +73,38 @@ function M.setup_lsp_keymaps(bufnr) { mode = 'n', lhs = 'gy', rhs = function() require('telescope.builtin').lsp_type_definitions() end, opts = { desc = 'LSP: Go to type definition' } }, -- Symbol navigation - { mode = 'n', lhs = 'ls', rhs = function() require('telescope.builtin').lsp_document_symbols() end, opts = { desc = 'LSP: Document symbols' } }, - { mode = 'n', lhs = 'lS', rhs = function() require('telescope.builtin').lsp_dynamic_workspace_symbols() end, opts = { desc = 'LSP: Workspace symbols' } }, + { mode = 'n', lhs = 'ls', rhs = function() + -- Explicitly add position_encoding to fix symbols_to_items error + require('telescope.builtin').lsp_document_symbols({ position_encoding = 'utf-16' }) + end, opts = { desc = 'LSP: Document symbols' } }, + { mode = 'n', lhs = 'lS', rhs = function() + -- Use a safe wrapper to avoid nil indexing errors + local status_ok, _ = pcall(function() + require('telescope.builtin').lsp_dynamic_workspace_symbols({ + position_encoding = 'utf-16', + show_line = true, + fname_width = 50, + symbol_width = 35, + attach_mappings = function(prompt_bufnr) + -- This prevents errors when no results are found + require("telescope.actions").select_default:replace(function() + local entry = require("telescope.actions.state").get_selected_entry() + if not entry then return end + require("telescope.actions").close(prompt_bufnr) + if entry.value and entry.value.filename then + vim.cmd(string.format('edit %s', entry.value.filename)) + vim.api.nvim_win_set_cursor(0, {entry.value.lnum, entry.value.col}) + end + end) + return true + end + }) + end) + + if not status_ok then + vim.notify("Workspace symbols not available for this language server", vim.log.levels.INFO) + end + end, opts = { desc = 'LSP: Workspace symbols' } }, -- Code actions { mode = 'n', lhs = 'lr', rhs = vim.lsp.buf.rename, opts = { desc = 'LSP: Rename symbol' } }, @@ -79,7 +112,7 @@ function M.setup_lsp_keymaps(bufnr) { mode = 'n', lhs = 'lf', rhs = function() vim.lsp.buf.format({ async = true }) end, opts = { desc = 'LSP: Format code' } }, -- Documentation - { mode = 'n', lhs = 'K', rhs = vim.lsp.buf.hover, opts = { desc = 'LSP: Show documentation' } }, + { mode = 'n', lhs = 'K', rhs = function() require('hover').hover() end, opts = { desc = 'Enhanced documentation (hover.nvim)' } }, } -- First clear any existing LSP keymaps for this buffer @@ -162,8 +195,9 @@ M.diagnostic_keymaps = { { mode = 'n', lhs = ']d', rhs = vim.diagnostic.goto_next, opts = { desc = 'Diagnostic: Next' } }, -- Viewing diagnostics - { mode = 'n', lhs = 'tt', rhs = vim.diagnostic.open_float, opts = { desc = 'Diagnostic: Show details' } }, + { mode = 'n', lhs = 'tt', rhs = vim.diagnostic.open_float, opts = { desc = 'Diagnostic: Show details in float' } }, { mode = 'n', lhs = 'tl', rhs = vim.diagnostic.setloclist, opts = { desc = 'Diagnostic: Show list' } }, + { mode = 'n', lhs = 'td', rhs = function() require('telescope.builtin').diagnostics() end, opts = { desc = 'Diagnostic: Search all diagnostics' } }, } -- Database keymaps (all under D for Database) @@ -223,12 +257,31 @@ M.scratch_keymaps = { -- - : Toggle terminal in float window M.snacks_keymaps = { -- Explorer - { mode = 'n', lhs = 'e', rhs = function() require('snacks.picker').explorer() end, opts = { desc = 'Explorer: Toggle' } }, - { mode = 'n', lhs = 'E', rhs = function() require('snacks.picker').explorer({ reveal = true }) end, opts = { desc = 'Explorer: Focus current file' } }, - { mode = 'n', lhs = 'o', rhs = function() require('snacks.picker').explorer({ reveal = true }) end, opts = { desc = 'Explorer: Focus current file' } }, + { mode = 'n', lhs = 'e', rhs = function() + -- Open explorer in current window + require('snacks').explorer.open() + end, opts = { desc = 'Explorer: Toggle' } }, + + { mode = 'n', lhs = 'E', rhs = function() + -- Reveal current file in explorer + require('snacks').explorer.reveal() + end, opts = { desc = 'Explorer: Focus current file' } }, + + { mode = 'n', lhs = 'o', rhs = function() + -- Open current file in a new tab and focus it + vim.cmd('tab split %') + end, opts = { desc = 'Open current file in new tab' } }, + + -- Custom function to open explorer files in new tabs + { mode = 'n', lhs = 'f', rhs = function() + -- Open explorer in a new tab + vim.cmd('tabnew') + require('snacks').explorer.open() + end, opts = { desc = 'Explorer: Open in new tab' } }, -- Terminal { mode = 'n', lhs = '', rhs = function() require('snacks').terminal.toggle() end, opts = { desc = 'Terminal: Toggle float window' } }, + { mode = 'n', lhs = 'tc', rhs = function() require('snacks').terminal.toggle() end, opts = { desc = 'Terminal: Toggle console' } }, } -- Git signs keymaps (all under g for Git) @@ -277,6 +330,35 @@ function M.setup_gitsigns_keymaps() end end +-- Elixir keymaps (using 'x' as the prefix - mnemonic for 'eXlixir') +M.elixir_keymaps = { + { mode = 'n', lhs = 'xt', + rhs = function() + require('elixir').run_test_file() + end, + opts = { desc = 'Elixir: Test file' } + }, + { mode = 'n', lhs = 'xn', + rhs = function() + require('elixir').run_nearest_test() + end, + opts = { desc = 'Elixir: Test nearest' } + }, + { mode = 'n', lhs = 'xm', + rhs = function() + vim.cmd('Telescope elixir mix') + end, + opts = { desc = 'Elixir: Mix tasks' } + }, +} + +-- Set up Elixir keymaps +function M.setup_elixir_keymaps() + for _, mapping in ipairs(M.elixir_keymaps or {}) do + vim.keymap.set(mapping.mode, mapping.lhs, mapping.rhs, mapping.opts) + end +end + -- Leap keymaps -- 's' for bidirectional search (both forward and backward) -- 'S' for searching in all windows @@ -327,7 +409,7 @@ local function init_keymaps() callback = function() -- Pause snacks explorer updates during write pcall(function() - local picker = require('snacks.picker') + local picker = require('plugins.snacks.picker') if picker.is_open() then picker.pause_updates() vim.schedule(function() @@ -405,12 +487,19 @@ local function init_keymaps() -- Set up database keymaps M.setup_dadbod_keymaps() + + -- Set up default tab navigation + vim.keymap.set('n', 'gt', ':tabnext', { silent = true, desc = 'Next tab' }) + vim.keymap.set('n', 'gT', ':tabprevious', { silent = true, desc = 'Previous tab' }) -- Set up session keymaps M.setup_session_keymaps() -- Set up git signs keymaps M.setup_gitsigns_keymaps() + + -- Set up Elixir keymaps + M.setup_elixir_keymaps() -- Set up leap keymaps M.setup_leap_keymaps() @@ -422,5 +511,29 @@ init_keymaps() -- Export `init_keymaps` as `M.setup` so `require('core.keymaps').setup()` works M.setup = init_keymaps +-- Mini-surround keymaps +-- These define how mini.surround plugin works with text objects +-- sa - add surroundings (visual mode or with motion like saiw) +-- sd - delete surroundings +-- sr - replace surroundings +-- sn - Find next surrounding +-- sN - Find previous surrounding +-- sh - Highlight surrounding +-- sf - Find surrounding (to use with textobject) +-- ss - Go to left surrounding +-- sS - Go to right surrounding +M.mini_surround_keymaps = { + add = 'sa', -- Add surrounding in Normal and Visual modes + delete = 'sd', -- Delete surrounding + find = 'sf', -- Find surrounding (to the right) + find_left = 'sF', -- Find surrounding (to the left) + highlight = 'sh', -- Highlight surrounding + replace = 'sr', -- Replace surrounding + update_n_lines = '', -- Update `n_lines` + + suffix_last = 'l', -- Suffix to search with 'prev' method + suffix_next = 'n', -- Suffix to search with 'next' method +} + -- Return the module return M diff --git a/lua/options/autocmds.lua b/lua/options/autocmds.lua index 88b87336..ebe0c0ca 100644 --- a/lua/options/autocmds.lua +++ b/lua/options/autocmds.lua @@ -11,19 +11,58 @@ function M.setup() end, }) - -- Auto format Go files on save - vim.api.nvim_create_autocmd('BufWritePre', { - pattern = '*.go', + + -- Add a smart format-on-save command that only uses null-ls + -- Fix for zig.vim ftplugin error - handles both JSON and struct output + vim.api.nvim_create_autocmd('FileType', { + pattern = 'zig', + group = vim.api.nvim_create_augroup('user-zig-fix', { clear = true }), callback = function() - vim.lsp.buf.format({ async = false }) + if vim.fn.executable('zig') ~= 1 or vim.g.zig_std_dir then return end + + -- First try with --json flag + local handle = io.popen('zig env --json 2>/dev/null') + if handle then + local result = handle:read('*a') + handle:close() + + -- Try to parse as JSON first + local success, env = pcall(vim.fn.json_decode, result) + if success and type(env) == 'table' and env.std_dir then + vim.g.zig_std_dir = env.std_dir + vim.opt_local.path:prepend(env.std_dir) + return + end + end + + -- If JSON parsing failed, try to parse the struct output + handle = io.popen('zig env 2>/dev/null') + if handle then + local result = handle:read('*a') + handle:close() + + -- Extract std_dir from struct output + local std_dir = result:match('%.std_dir%s*=%s*"([^"]+)"') + if std_dir then + vim.g.zig_std_dir = std_dir + vim.opt_local.path:prepend(std_dir) + end + end end, }) - -- Auto format Zig files on save vim.api.nvim_create_autocmd('BufWritePre', { - pattern = '*.zig', - callback = function() - vim.lsp.buf.format({ async = false }) + pattern = '*', + callback = function(args) + local bufnr = args.buf + -- Check if null-ls is the formatter for this buffer + local client = vim.lsp.get_clients({ name = 'null-ls', bufnr = bufnr })[1] + if not client then + return + end + + -- Format the buffer + vim.lsp.buf.format({ bufnr = bufnr, filter = function(c) return c.name == 'null-ls' end, async = false }) end, }) end diff --git a/lua/plugins/better-hover.lua b/lua/plugins/better-hover.lua new file mode 100644 index 00000000..d79004b6 --- /dev/null +++ b/lua/plugins/better-hover.lua @@ -0,0 +1,58 @@ +---@diagnostic disable: undefined-global +-- Direct approach to fix hover documentation with custom function +return { + "neovim/nvim-lspconfig", + config = function() + -- Create a direct standalone hover function + local function better_hover() + -- Save original functions we'll temporarily override + local orig_open_floating = vim.lsp.util.open_floating_preview + + -- Replace the floating preview function temporarily + vim.lsp.util.open_floating_preview = function(contents, syntax, opts) + opts = opts or {} + -- Force nicer styling + opts.border = "rounded" + opts.max_width = math.min(math.floor(vim.o.columns * 0.7), 80) + opts.max_height = math.floor(vim.o.lines * 0.3) + + -- Process contents to remove separator lines and improve formatting + local cleaned_contents = {} + for _, line in ipairs(contents) do + -- Skip separator lines + if not line:match("^%-%-%-%-+$") then + table.insert(cleaned_contents, line) + end + end + + -- Call original with improved options and cleaned content + local bufnr, winnr = orig_open_floating(cleaned_contents, syntax, opts) + + -- Enhance the created buffer + if bufnr and winnr then + -- Set better options for the window + vim.api.nvim_win_set_option(winnr, "conceallevel", 2) + vim.api.nvim_win_set_option(winnr, "foldenable", false) + + -- If it's markdown, ensure proper highlighting + if syntax == "markdown" then + vim.api.nvim_buf_set_option(bufnr, "filetype", "markdown") + end + end + + return bufnr, winnr + end + + -- Call the regular hover function with our temporary override + vim.lsp.buf.hover() + + -- Restore original function + vim.lsp.util.open_floating_preview = orig_open_floating + end + + -- Directly override the K keymap - this bypasses any conflicts + vim.keymap.set("n", "K", better_hover, { noremap = true, silent = true, desc = "LSP: Better hover docs" }) + end, + -- Run this after all other LSP configs are loaded + priority = 999, +} diff --git a/lua/plugins/blink-cmp.lua b/lua/plugins/blink-cmp.lua deleted file mode 100644 index d1ec77a6..00000000 --- a/lua/plugins/blink-cmp.lua +++ /dev/null @@ -1,12 +0,0 @@ -return { - 'saghen/blink.cmp', - dependencies = 'rafamadriz/friendly-snippets', - version = '*', - opts = { - keymap = { preset = 'default' }, - appearance = { - use_nvim_cmp_as_default = true, - nerd_font_variant = 'mono', - }, - }, -} diff --git a/lua/plugins/cmp/setup.lua b/lua/plugins/cmp/setup.lua index e02e754d..25ffaa9b 100644 --- a/lua/plugins/cmp/setup.lua +++ b/lua/plugins/cmp/setup.lua @@ -5,6 +5,8 @@ local M = {} function M.setup() local cmp = require 'cmp' local luasnip = require 'luasnip' + local types = require 'cmp.types' + local compare = require 'cmp.config.compare' luasnip.config.setup {} cmp.setup { @@ -13,13 +15,127 @@ function M.setup() luasnip.lsp_expand(args.body) end, }, - completion = { completeopt = 'menu,menuone,noinsert' }, + -- Improved completion settings - more like VSCode + completion = { + completeopt = 'menu,menuone,noinsert', + keyword_length = 1, -- Show completions after 1 character + autocomplete = { types.cmp.TriggerEvent.TextChanged }, + get_trigger_characters = function(trigger_characters) + -- Always add '.' as a trigger character to ensure method/property completions + if not vim.tbl_contains(trigger_characters, '.') then + table.insert(trigger_characters, '.') + end + if not vim.tbl_contains(trigger_characters, ':') then + table.insert(trigger_characters, ':') + end + return trigger_characters + end, + }, + -- Custom sorting to prioritize fields and methods over snippets + sorting = { + priority_weight = 10, -- Increase weight to make priority differences more significant + comparators = { + -- Custom comparator that puts snippets at the end + function(entry1, entry2) + local types = require('cmp.types') + + -- Extract the kinds + local kind1 = entry1:get_kind() + local kind2 = entry2:get_kind() + + -- Define snippet kinds + local snippet_kinds = { + [types.lsp.CompletionItemKind.Snippet] = true + } + + -- Check if either is a snippet + local is_snippet1 = snippet_kinds[kind1] or false + local is_snippet2 = snippet_kinds[kind2] or false + + -- If one is a snippet and the other isn't, the non-snippet comes first + if is_snippet1 and not is_snippet2 then + return false + elseif not is_snippet1 and is_snippet2 then + return true + end + + -- Otherwise fall through to other comparators + return nil + end, + compare.exact, -- Exact match first + compare.kind, -- Sort by kind (functions first, then properties, etc) + compare.sort_text, -- Sort by LSP's sortText + compare.score, -- Score based + compare.offset, -- Closest to cursor + compare.length, -- Shorter first + compare.order, -- Source order + }, + }, + -- Enhanced VSCode-like icons with more modern symbols + formatting = { + format = function(entry, vim_item) + -- Set icons for completion types + local kind_icons = { + Text = "󰉿", Method = "󰆧", Function = "󰊕", Constructor = "", + Field = "󰜢", Variable = "󰀫", Class = "󰠱", Interface = "", + Module = "", Property = "󰜢", Unit = "", Value = "󰎠", + Enum = "", Keyword = "󰌋", Snippet = "", Color = "󰏘", + File = "󰈙", Reference = "", Folder = "󰉋", EnumMember = "", + Constant = "󰏿", Struct = "", Event = "", Operator = "󰆕", + TypeParameter = "󰊄", TypeAlias = "", Parameter = "", StaticMethod = "", + Macro = "󰁌" + } + + -- Enhanced formatting with better icons and spacing + vim_item.kind = string.format('%s %s', kind_icons[vim_item.kind] or '', vim_item.kind) + + -- Show more detailed source info + vim_item.menu = ({ + buffer = "[Buffer]", + nvim_lsp = "[LSP]", + luasnip = "[Snippet]", + path = "[Path]", + })[entry.source.name] + + return vim_item + end, + }, + -- Enhanced documentation window + window = { + completion = cmp.config.window.bordered({ + winhighlight = "Normal:CmpPmenu,FloatBorder:CmpPmenuBorder,CursorLine:PmenuSel,Search:None", + scrollbar = true, + col_offset = -3, + side_padding = 1, + }), + documentation = cmp.config.window.bordered({ + winhighlight = "Normal:CmpDoc", + }), + }, mapping = cmp.mapping.preset.insert { [''] = cmp.mapping.select_next_item(), [''] = cmp.mapping.select_prev_item(), [''] = cmp.mapping.scroll_docs(-4), [''] = cmp.mapping.scroll_docs(4), [''] = cmp.mapping.confirm { select = true }, + [''] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_next_item() + elseif luasnip.expand_or_locally_jumpable() then + luasnip.expand_or_jump() + else + fallback() + end + end, { 'i', 's' }), + [''] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_prev_item() + elseif luasnip.locally_jumpable(-1) then + luasnip.jump(-1) + else + fallback() + end + end, { 'i', 's' }), [''] = cmp.mapping.complete {}, [''] = cmp.mapping(function() if luasnip.expand_or_locally_jumpable() then @@ -32,30 +148,112 @@ function M.setup() end end, { 'i', 's' }), }, - sources = { - { name = 'lazydev', group_index = 0 }, - { name = 'nvim_lsp' }, - { name = 'luasnip' }, - { name = 'path' }, - { name = 'buffer' }, + -- Configure completion sources by priority with tweaked settings + sources = cmp.config.sources( + { + { name = 'nvim_lsp', priority = 1000, max_item_count = 50 }, -- LSP comes first with more items + { name = 'path', priority = 500 }, -- Then paths + { name = 'buffer', priority = 250, keyword_length = 3 }, -- Then current buffer + -- Snippets with extremely low priority and limited visibility + { name = 'luasnip', priority = 10, max_item_count = 3, keyword_length = 4 }, + } + ), + -- Enable experimental features for VSCode-like experience + experimental = { + ghost_text = true, -- Shows virtual text as preview }, } -- Cmdline completion for search (/, ?) and command (:) + -- Enhanced commandline completion with better history and formatting cmp.setup.cmdline({ '/', '?' }, { mapping = cmp.mapping.preset.cmdline(), sources = { - { name = 'buffer' } + { name = 'buffer', max_item_count = 20 } + }, + window = { + completion = cmp.config.window.bordered({ + winhighlight = "Normal:CmpPmenu,FloatBorder:CmpPmenuBorder,CursorLine:PmenuSel,Search:None", + scrollbar = true, + }) + }, + completion = { + completeopt = 'menu,menuone,noinsert', } }) cmp.setup.cmdline(':', { - mapping = cmp.mapping.preset.cmdline(), + mapping = cmp.mapping.preset.cmdline({ + -- Explicit key mappings for command mode completion + [''] = { + c = function(fallback) + if cmp.visible() then + cmp.select_next_item() + else + cmp.complete() + cmp.select_next_item() + end + end, + }, + [''] = { + c = function(fallback) + if cmp.visible() then + cmp.select_prev_item() + else + cmp.complete() + cmp.select_prev_item() + end + end, + }, + [''] = { + c = function(fallback) + if cmp.visible() then + cmp.select_next_item() + else + cmp.complete() + end + end, + }, + [''] = { + c = function(fallback) + if cmp.visible() then + cmp.select_prev_item() + else + cmp.complete() + end + end, + }, + [''] = { + c = cmp.mapping.abort(), + }, + [''] = { + c = cmp.mapping.confirm({ select = true }), + }, + }), sources = cmp.config.sources({ - { name = 'path' } - }, { - { name = 'cmdline' } - }) + { name = 'path', max_item_count = 20 }, + { name = 'cmdline', max_item_count = 30, keyword_length = 1 } + }), + window = { + completion = cmp.config.window.bordered({ + winhighlight = "Normal:CmpPmenu,FloatBorder:CmpPmenuBorder,CursorLine:PmenuSel,Search:None", + scrollbar = true, + col_offset = -3, + side_padding = 1, + }) + }, + formatting = { + format = function(entry, vim_item) + -- Only show item kind for cmdline completion + vim_item.kind = ' ' + vim_item.menu = '' + return vim_item + end, + }, + completion = { + completeopt = 'menu,menuone,noinsert', + autocomplete = { cmp.TriggerEvent.TextChanged }, + } }) end diff --git a/lua/plugins/coding.lua b/lua/plugins/coding.lua index 3b4e3809..1e0c8bb1 100644 --- a/lua/plugins/coding.lua +++ b/lua/plugins/coding.lua @@ -4,4 +4,5 @@ return { require('plugins.coding.zig'), require('plugins.coding.clangd'), require('plugins.coding.dap'), + require('plugins.coding.elixir'), } diff --git a/lua/plugins/coding/dap.lua b/lua/plugins/coding/dap.lua index 97f0a5a1..3ca97094 100644 --- a/lua/plugins/coding/dap.lua +++ b/lua/plugins/coding/dap.lua @@ -1,6 +1,16 @@ ---@diagnostic disable: undefined-global return { "mfussenegger/nvim-dap", + keys = { + { "pb", desc = "Toggle [B]reakpoint" }, + { "pc", desc = "[C]ontinue debugging" }, + { "pn", desc = "Step over ([N]ext)" }, + { "pi", desc = "Step [I]nto" }, + { "po", desc = "Step [O]ut" }, + { "pr", desc = "Open [R]EPL" }, + { "pl", desc = "Run [L]ast debug session" }, + { "px", desc = "Toggle debug UI" }, + }, dependencies = { "rcarriga/nvim-dap-ui", "theHamsta/nvim-dap-virtual-text", diff --git a/lua/plugins/coding/elixir.lua b/lua/plugins/coding/elixir.lua new file mode 100644 index 00000000..3b0651b6 --- /dev/null +++ b/lua/plugins/coding/elixir.lua @@ -0,0 +1,38 @@ +return { + { + 'elixir-tools/elixir-tools.nvim', + version = '*', + ft = { 'ex', 'exs', 'heex', 'eex', 'elixir' }, + dependencies = { + 'neovim/nvim-lspconfig', + 'nvim-lua/plenary.nvim', + }, + config = function() + local elixir = require('elixir') + + -- Configure elixir-tools with LSP disabled (handled by lspconfig) + elixir.setup({ + -- Disable the built-in LSP client to avoid conflicts + elixirls = { enable = false }, + + -- Enable non-LSP features + credo = { enable = true }, + projectionist = { enable = true }, + + -- Configure the test runner + test_runner = { + enabled = true, + runner = 'exunit', -- or 'exunit_individual' for individual test runs + }, + + -- Enable mix integration + mix = { + enabled = true, + format_on_save = false, -- Disabled to prevent file changed warnings + }, + }) + + -- Keymaps are now in core/keymaps.lua with lx prefix + end, + }, +} diff --git a/lua/plugins/doc-test.lua b/lua/plugins/doc-test.lua new file mode 100644 index 00000000..596b4715 --- /dev/null +++ b/lua/plugins/doc-test.lua @@ -0,0 +1,76 @@ +---@diagnostic disable: undefined-global +-- Test doc window plugin +return { + "neovim/nvim-lspconfig", + config = function() + -- Create a test function that shows a styled floating window + local function test_doc_window() + local contents = { + "## Documentation Test", + "", + "This is a test documentation window to see how styling works.", + "", + "**Important**: This shows what documentation *should* look like.", + "", + "```lua", + "function example(param)", + " -- This is a code sample", + " return param + 1", + "end", + "```", + } + + -- Create a nicely styled window + local opts = { + border = "rounded", + width = 60, + height = 12, + wrap = true, + pad_left = 1, + pad_right = 1, + } + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, contents) + vim.api.nvim_buf_set_option(bufnr, "filetype", "markdown") + + -- Position the window near the cursor + local cursor_pos = vim.api.nvim_win_get_cursor(0) + local row = cursor_pos[1] + local col = cursor_pos[2] + + -- Calculate position + local ui = vim.api.nvim_list_uis()[1] + local width = opts.width or 60 + local height = opts.height or 12 + local anchor = "NW" + local col_offset = 0 + + -- Open the window + local winnr = vim.api.nvim_open_win(bufnr, false, { + relative = "cursor", + row = 1, + col = col_offset, + width = width, + height = height, + style = "minimal", + border = opts.border, + anchor = anchor, + }) + + -- Set window options + vim.api.nvim_win_set_option(winnr, "conceallevel", 2) + vim.api.nvim_win_set_option(winnr, "concealcursor", "n") + vim.api.nvim_win_set_option(winnr, "winblend", 0) + + -- Return window and buffer numbers + return bufnr, winnr + end + + -- Add a command to test the documentation window + vim.api.nvim_create_user_command("TestDocWindow", test_doc_window, {}) + + -- Also map it to a key for easy testing + vim.keymap.set("n", "td", test_doc_window, { desc = "Test doc window styling" }) + end +} diff --git a/lua/plugins/hover.lua b/lua/plugins/hover.lua new file mode 100644 index 00000000..c8a187ce --- /dev/null +++ b/lua/plugins/hover.lua @@ -0,0 +1,44 @@ +---@diagnostic disable: undefined-global +return { + 'lewis6991/hover.nvim', + event = 'VeryLazy', + config = function() + require('hover').setup({ + init = function() + -- Required for hover.nvim + require('hover.providers.lsp') + -- Optional additional providers + require('hover.providers.gh') + require('hover.providers.man') + require('hover.providers.dictionary') + end, + preview_opts = { + border = 'rounded', + width = 80, + height = 20, + }, + title = true, + -- Key mappings + -- Use the same mapping as before ('K') + mappings = { + toggle_hover = 'K', + -- Scroll option + scroll_up = '', + scroll_down = '', + }, + }) + + -- Style adjustments + vim.api.nvim_set_hl(0, 'HoverFloat', { link = 'Normal' }) + vim.api.nvim_set_hl(0, 'HoverFloatBorder', { link = 'FloatBorder' }) + + -- Override the LSP hover handler to ensure our hover.nvim is used + vim.api.nvim_create_autocmd('LspAttach', { + callback = function(args) + -- Clear and override the K mapping to ensure it uses hover.nvim + vim.keymap.set('n', 'K', require('hover').hover, + { buffer = args.buf, desc = 'Enhanced documentation (hover.nvim)' }) + end, + }) + end, +} diff --git a/lua/plugins/lsp/overrides.lua b/lua/plugins/lsp/overrides.lua index 6cfa14f9..9f7e5ad8 100644 --- a/lua/plugins/lsp/overrides.lua +++ b/lua/plugins/lsp/overrides.lua @@ -4,41 +4,57 @@ local M = {} function M.setup() - -- Create a wrapper that ensures position_encoding is added to all calls - local function with_encoding(fn) - return function(params, ...) - params = params or {} - if not params.position_encoding then - params.position_encoding = 'utf-16' - end - return fn(params, ...) - end - end - - -- Override standard LSP buffer functions that may be missing position_encoding - vim.lsp.buf.code_action = with_encoding(vim.lsp.buf.code_action) - vim.lsp.buf.rename = with_encoding(vim.lsp.buf.rename) - vim.lsp.buf.hover = with_encoding(vim.lsp.buf.hover) - vim.lsp.buf.formatting = with_encoding(vim.lsp.buf.formatting) - vim.lsp.buf.range_formatting = with_encoding(vim.lsp.buf.range_formatting) - vim.lsp.buf.format = with_encoding(vim.lsp.buf.format) - - -- Patch util functions as well - local orig_make_position_params = vim.lsp.util.make_position_params - vim.lsp.util.make_position_params = function(window, client, position_encoding) - if not position_encoding then - position_encoding = 'utf-16' - end - return orig_make_position_params(window, client, position_encoding) - end - - -- Override symbols_to_items to ensure position_encoding is set - local orig_symbols_to_items = vim.lsp.util.symbols_to_items + -- Patch the symbols_to_items function to always use utf-16 encoding + -- and handle nil values safely + local original_symbols_to_items = vim.lsp.util.symbols_to_items vim.lsp.util.symbols_to_items = function(symbols, bufnr, position_encoding) - if not position_encoding then - position_encoding = 'utf-16' + -- Default to utf-16 if not specified + position_encoding = position_encoding or 'utf-16' + + -- Add extra safety checks for nil values + if not symbols or type(symbols) ~= 'table' then + return {} + end + + -- Call original with proper encoding + return original_symbols_to_items(symbols, bufnr, position_encoding) + end + + -- Patch workspace_symbol handler to be safer + vim.lsp.handlers['workspace/symbol'] = function(err, result, ctx, config) + if err or not result or vim.tbl_isempty(result) then + return {} + end + + -- Set a default position_encoding + local position_encoding = 'utf-16' + local client = vim.lsp.get_client_by_id(ctx.client_id) + if client and client.offset_encoding then + position_encoding = client.offset_encoding + end + + -- Process safely + local items = vim.lsp.util.symbols_to_items(result, nil, position_encoding) or {} + return items + end + + -- Patch all lsp_* functions in telescope.builtin to include position_encoding + local telescope_builtin = require('telescope.builtin') + local telescope_funcs = { + 'lsp_references', 'lsp_definitions', 'lsp_implementations', + 'lsp_type_definitions', 'lsp_document_symbols', 'lsp_workspace_symbols', + 'lsp_dynamic_workspace_symbols' + } + + for _, func_name in ipairs(telescope_funcs) do + local original_func = telescope_builtin[func_name] + telescope_builtin[func_name] = function(opts) + opts = opts or {} + if not opts.position_encoding then + opts.position_encoding = 'utf-16' + end + return original_func(opts) end - return orig_symbols_to_items(symbols, bufnr, position_encoding) end end diff --git a/lua/plugins/lsp/servers.lua b/lua/plugins/lsp/servers.lua index 3b8d9606..89327ed1 100644 --- a/lua/plugins/lsp/servers.lua +++ b/lua/plugins/lsp/servers.lua @@ -55,7 +55,72 @@ return { }, }, -- C/C++ - clangd = {}, + clangd = { + cmd = { + 'clangd', + '--background-index', + '--clang-tidy', + '--header-insertion=iwyu', + '--completion-style=detailed', + '--function-arg-placeholders', + '--fallback-style=llvm', + }, + init_options = { + usePlaceholders = true, + completeUnimported = true, + clangdFileStatus = true, + }, + on_attach = function(client, bufnr) + -- Set the correct standard based on filetype + local filetype = vim.bo[bufnr].filetype + if filetype == 'c' then + client.config.init_options.fallbackFlags = { '-std=c23' } + elseif filetype == 'cpp' then + client.config.init_options.fallbackFlags = { '-std=c++23' } + end + + -- Call the default on_attach to setup keymaps and navic + require('plugins.lsp.setup').default_on_attach(client, bufnr) + end, + settings = { + clangd = { + InlayHints = { + Designators = true, + Enabled = true, + ParameterNames = true, + DeducedTypes = true, + }, + }, + }, + }, + + -- Zig + zls = { + settings = { + zls = { + -- Enable semantic highlighting + enable_semantic_tokens = true, + -- Enable inlay hints for better code understanding + enable_inlay_hints = true, + -- Enable snippets + enable_snippets = true, + -- Enable auto-fixing + enable_autofix = true, + -- Warn about style issues + warn_style = true, + -- Highlight global variables + highlight_global_var_declarations = true, + -- Enable build-on-save for faster feedback + enable_build_on_save = true, + -- Skip std library references for better performance + skip_std_references = false, + -- Prefer ast-check over zig fmt for faster response + prefer_ast_check_as_child_process = true, + -- Enable import embedfile argument completions + enable_import_embedfile_argument_completions = true, + }, + }, + }, -- SQL sqls = { @@ -66,4 +131,17 @@ return { }, }, }, + + -- Elixir + elixirls = { + cmd = { 'elixir-ls' }, + settings = { + elixirLS = { + dialyzerEnabled = true, + fetchDeps = true, + enableTestLenses = true, + suggestSpecs = true, + } + } + } } diff --git a/lua/plugins/lsp/setup.lua b/lua/plugins/lsp/setup.lua index d5cb7b90..4ea1960e 100644 --- a/lua/plugins/lsp/setup.lua +++ b/lua/plugins/lsp/setup.lua @@ -4,10 +4,50 @@ local M = {} function M.setup() + -- Apply LSP function overrides for position_encoding + require('plugins.lsp.overrides').setup() + local servers = require('plugins.lsp.servers') - -- nvim-cmp capabilities + -- Enhanced nvim-cmp capabilities (VSCode-like) local capabilities = vim.lsp.protocol.make_client_capabilities() + capabilities.textDocument.completion.completionItem = { + documentationFormat = { 'markdown', 'plaintext' }, + snippetSupport = true, + preselectSupport = true, + insertReplaceSupport = true, + labelDetailsSupport = true, + deprecatedSupport = true, + commitCharactersSupport = true, + tagSupport = { valueSet = { 1 } }, + resolveSupport = { + properties = { + 'documentation', + 'detail', + 'additionalTextEdits', + }, + }, + } + -- Complete with all available methods and properties + capabilities.textDocument.completion.completionList = { + itemDefaults = { + 'commitCharacters', + 'editRange', + 'insertTextFormat', + 'insertTextMode', + 'data', + }, + } + -- Add semantic tokens for better syntax highlighting + capabilities.textDocument.semanticTokens = { + tokenTypes = { 'namespace', 'type', 'class', 'enum', 'interface', 'struct', 'typeParameter', 'parameter', 'variable', 'property', 'enumMember', 'event', 'function', 'method', 'macro', 'keyword', 'modifier', 'comment', 'string', 'number', 'regexp', 'operator', 'decorator' }, + tokenModifiers = { 'declaration', 'definition', 'readonly', 'static', 'deprecated', 'abstract', 'async', 'modification', 'documentation', 'defaultLibrary' }, + formats = { 'relative' }, + requests = { + range = true, + full = true, + }, + } capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities) -- Setup mason-lspconfig @@ -20,16 +60,27 @@ function M.setup() config.capabilities = capabilities -- Default on_attach to setup keymaps & navic - local function on_attach(client, bufnr) + M.default_on_attach = function(client, bufnr) require('core.keymaps').setup_lsp_keymaps(bufnr) - -- Attach navic if available - if client.server_capabilities.documentSymbolProvider then - require('nvim-navic').attach(client, bufnr) + + -- Only attach navic if: + -- 1. The client supports document symbols + -- 2. The client isn't elixirls (handled by elixir-tools.nvim) + if client.server_capabilities.documentSymbolProvider + and client.name ~= 'elixirls' then + local status_ok, _ = pcall(require, 'nvim-navic') + if status_ok then + require('nvim-navic').attach(client, bufnr) + end end end - config.on_attach = on_attach + + -- Only set the default on_attach if one isn't already provided + if not config.on_attach then + config.on_attach = M.default_on_attach + end - -- Disable gopls semantic tokens to avoid issues + -- Enhance LSP capabilities for specific servers if server_name == 'gopls' and config.on_attach then local orig_on_attach = config.on_attach config.on_attach = function(client, bufnr) @@ -37,11 +88,56 @@ function M.setup() orig_on_attach(client, bufnr) end end + + -- Force all LSP servers to use incremental completions (like VSCode) + if not config.settings then config.settings = {} end + if not config.settings.completions then config.settings.completions = {} end + config.settings.completions.completeFunctionCalls = true + + -- Enhanced settings for Lua LSP + if server_name == 'lua_ls' then + if not config.settings.Lua then config.settings.Lua = {} end + config.settings.Lua.completion = { + callSnippet = "Replace", + showWord = "Disable", + workspaceWord = false, + displayContext = 5, + keywordSnippet = "Both", + postfix = ".", + } + -- More comprehensive inferencing + config.settings.Lua.hint = { + enable = true, + setType = true, + paramType = true, + paramName = "Literal", + arrayIndex = "Enable", + } + -- Better type resolution + config.settings.Lua.type = { + castNumberToInteger = true, + } + -- Add standard Lua libraries for better completion + if not config.settings.Lua.workspace then config.settings.Lua.workspace = {} end + if not config.settings.Lua.workspace.library then config.settings.Lua.workspace.library = {} end + + -- Include standard libraries + local lua_types = vim.fn.expand('~/.local/share/nvim/mason/packages/lua-language-server/libexec/meta/3rd') + if vim.fn.isdirectory(lua_types) == 1 then + config.settings.Lua.workspace.library[lua_types] = true + end + -- Enable all features + config.settings.Lua.hover = { enable = true, expandAlias = true } + config.settings.Lua.signatureHelp = { enable = true } + config.settings.Lua.diagnostics = { enable = true, globals = { "vim" } } + end require('lspconfig')[server_name].setup(config) end, } + -- Hover handler will now be provided by Blink + -- Override references handler vim.lsp.handlers['textDocument/references'] = function(err, result, ctx, config) if not result or vim.tbl_isempty(result) then diff --git a/lua/plugins/null-ls.lua b/lua/plugins/null-ls.lua index 3c41d5a8..d1e64f41 100644 --- a/lua/plugins/null-ls.lua +++ b/lua/plugins/null-ls.lua @@ -1,10 +1,43 @@ -return { +---@diagnostic disable: undefined-global +local vim = vim + +local M = { 'nvimtools/none-ls.nvim', event = { 'BufReadPre', 'BufNewFile' }, dependencies = { 'jay-babu/mason-null-ls.nvim', }, config = function() - require('plugins.null-ls.setup').setup() + -- Safely require null-ls + local ok, null_ls = pcall(require, 'null-ls') + if not ok then + vim.notify('Failed to load null-ls', vim.log.levels.ERROR) + return + end + + -- Safely require setup + local setup_ok, setup = pcall(require, 'plugins.null-ls.setup') + if not setup_ok or type(setup.setup) ~= 'function' then + vim.notify('Failed to load null-ls setup', vim.log.levels.ERROR) + return + end + + -- Initialize null-ls + setup.setup() + + -- Additional safety check for sources + vim.defer_fn(function() + if not null_ls or not null_ls.get_sources then + vim.notify('null-ls sources not available', vim.log.levels.WARN) + return + end + + local sources = null_ls.get_sources() + if #sources == 0 then + vim.notify('No null-ls sources registered', vim.log.levels.WARN) + end + end, 1000) -- Check after 1 second end, } + +return M diff --git a/lua/plugins/null-ls/setup.lua b/lua/plugins/null-ls/setup.lua index d36235ed..2159d2d3 100644 --- a/lua/plugins/null-ls/setup.lua +++ b/lua/plugins/null-ls/setup.lua @@ -9,6 +9,7 @@ function M.setup() local diagnostics = null_ls.builtins.diagnostics null_ls.setup { + root_dir = null_ls_utils.root_pattern('.null-ls-root', 'Makefile', '.git'), timeout = 10000, debounce = 250, diff --git a/lua/plugins/nvim-cmp.lua b/lua/plugins/nvim-cmp.lua index 62b45888..25054fd6 100644 --- a/lua/plugins/nvim-cmp.lua +++ b/lua/plugins/nvim-cmp.lua @@ -19,12 +19,12 @@ return { -- Autocompletion -- `friendly-snippets` contains a variety of premade snippets. -- See the README about individual language/framework/plugin snippets: -- https://github.com/rafamadriz/friendly-snippets - -- { - -- 'rafamadriz/friendly-snippets', - -- config = function() - -- require('luasnip.loaders.from_vscode').lazy_load() - -- end, - -- }, + { + 'rafamadriz/friendly-snippets', + config = function() + require('luasnip.loaders.from_vscode').lazy_load() + end, + }, }, }, 'saadparwaiz1/cmp_luasnip', diff --git a/lua/plugins/nvim-lspconfig.lua b/lua/plugins/nvim-lspconfig.lua index 55134df3..e672a006 100644 --- a/lua/plugins/nvim-lspconfig.lua +++ b/lua/plugins/nvim-lspconfig.lua @@ -4,13 +4,42 @@ local go_flags = 'integration' -- add the tags here, instead of searching it bel return { -- Main LSP Configuration 'neovim/nvim-lspconfig', + event = { "BufReadPre", "BufNewFile" }, dependencies = { -- Automatically install LSPs and related tools to stdpath for Neovim - { 'williamboman/mason.nvim', config = true }, -- NOTE: Must be loaded before dependants - 'williamboman/mason-lspconfig.nvim', + { + 'williamboman/mason.nvim', + cmd = { "Mason", "MasonInstall", "MasonUpdate" }, + config = function() + require("mason").setup({ + ui = { + icons = { + package_installed = "✓", + package_pending = "➜", + package_uninstalled = "✗" + }, + border = "rounded", + width = 0.8, + height = 0.9, + }, + log_level = vim.log.levels.INFO, + max_concurrent_installers = 4, + }) + end + }, + { + 'williamboman/mason-lspconfig.nvim', + dependencies = { 'williamboman/mason.nvim' }, + config = function() + require("mason-lspconfig").setup({ + automatic_installation = true + }) + end + }, -- 'folke/neodev.nvim', -- Adds support for Neovim Lua API -- No longer needed with lazydev { 'WhoIsSethDaniel/mason-tool-installer.nvim', + event = "VeryLazy", config = function() require('mason-tool-installer').setup { ensure_installed = { @@ -35,6 +64,8 @@ return { 'debugpy', -- Python debugger -- SQL tools 'sqls', -- Advanced SQL LSP + -- Elixir + 'elixir-ls' -- Elixir LSP }, auto_update = true, run_on_start = true, diff --git a/lua/plugins/nvim-treesitter.lua b/lua/plugins/nvim-treesitter.lua index fdb86626..0a38e72d 100644 --- a/lua/plugins/nvim-treesitter.lua +++ b/lua/plugins/nvim-treesitter.lua @@ -1,22 +1,49 @@ -return { -- Highlight, edit, and navigate code +return { + -- Highlight, edit, and navigate code 'nvim-treesitter/nvim-treesitter', - build = ':TSUpdate', + dependencies = { + 'nvim-treesitter/nvim-treesitter-textobjects', + }, + build = function() + local ts_update = require('nvim-treesitter.install').update({ with_sync = true }) + ts_update() + -- Install C++ parser explicitly to avoid tarball issues + vim.cmd('silent! TSInstall cpp') + end, main = 'nvim-treesitter.configs', -- Sets main module to use for opts -- [[ Configure Treesitter ]] See `:help nvim-treesitter` opts = { - ensure_installed = { 'bash', 'c', 'diff', 'html', 'lua', 'luadoc', 'markdown', 'markdown_inline', 'query', 'vim', 'vimdoc' }, + -- List of languages to ensure are installed + ensure_installed = { + 'bash', 'c', 'cpp', 'diff', 'html', 'lua', 'luadoc', + 'markdown', 'markdown_inline', 'python', 'rust', + 'javascript', 'typescript', 'json', 'yaml', 'query', + 'vim', 'vimdoc', 'comment', 'regex' + }, + -- Install parsers synchronously (only applied to `ensure_installed`) sync_install = false, + -- Automatically install missing parsers when entering buffer auto_install = true, - ignore_install = {}, + -- List of parsers to ignore installing (for "all") + ignore_install = { 'phpdoc' }, highlight = { enable = true, - -- Some languages depend on vim's regex highlighting system (such as Ruby) for indent rules. - -- If you are experiencing weird indenting issues, add the language to - -- the list of additional_vim_regex_highlighting and disabled languages for indent. + -- Additional filetypes to enable highlighting for additional_vim_regex_highlighting = { 'ruby' }, + -- Disable for large files + disable = function(_, buf) + local max_filesize = 100 * 1024 -- 100 KB + local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(buf)) + if ok and stats and stats.size > max_filesize then + return true + end + end, + }, + indent = { + enable = true, + disable = { 'ruby', 'yaml' } }, - indent = { enable = true, disable = { 'ruby' } }, }, -- There are additional nvim-treesitter modules that you can use to interact -- with nvim-treesitter. You should go explore a few and see what interests you: diff --git a/lua/plugins/which-key.lua b/lua/plugins/which-key.lua index 332f39a5..5080897e 100644 --- a/lua/plugins/which-key.lua +++ b/lua/plugins/which-key.lua @@ -1,3 +1,4 @@ +---@diagnostic disable: undefined-global return { -- Useful plugin to show you pending keybinds. 'folke/which-key.nvim', event = 'VimEnter', -- Sets the loading event to 'VimEnter' @@ -63,12 +64,12 @@ return { -- Useful plugin to show you pending keybinds. { 'l', group = '[L]SP/Language' }, { 'm', group = '[M]emory/Sessions' }, { 'D', group = '[D]atabase' }, - { 't', group = '[T]oggle', desc = { - h = 'Toggle inlay [H]ints', - }}, - { 'w', group = '[W]orkspace', desc = { - s = '[S]ymbols', + { 't', group = '[T]roubleshooting', desc = { + d = 'Search all [D]iagnostics', + l = 'Diagnostics in [L]ocation list', + t = 'Diagnostic details in floa[T]', }}, + { 'w', desc = 'Save file' }, { 'h', group = 'Git [H]unk', mode = { 'n', 'v' } }, { 'g', group = '[G]it', desc = { s = 'Status', @@ -105,6 +106,7 @@ return { -- Useful plugin to show you pending keybinds. { 'X', desc = 'Force close buffer' }, { '/', desc = 'Search in current buffer' }, { '', desc = 'Find buffers' }, + { '', desc = 'Search text in all files' }, }, }, }