diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9ac3fe3..616d0f3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -29,23 +29,35 @@ Modern Lua-first Neovim configuration using lazy.nvim, currently in active migra ### 1. LSP Configuration (Phase 3 Complete) **Modern API (Neovim 0.11+)**: Uses `vim.lsp.config()` + `vim.lsp.enable()`, NOT `require('lspconfig')[server].setup()`. +**CRITICAL (2025-12-07)**: Neovim 0.11+ does NOT support `on_new_config` callback. Use `LspAttach` autocmd instead. + ```lua -- lua/plugins/lsp.lua pattern: +-- Define config vim.lsp.config(server_name, { capabilities = capabilities, - on_attach = on_attach, - on_new_config = function(new_config, root_dir) - -- Load project settings from root_dir/.nvim.lua + -- NO on_attach here, NO on_new_config here +}) + +-- Enable server +vim.lsp.enable({ "lua_ls", "intelephense", ... }) + +-- Load project settings via LspAttach autocmd +vim.api.nvim_create_autocmd('LspAttach', { + callback = function(args) + local client = vim.lsp.get_client_by_id(args.data.client_id) + -- Load .nvim.lua from client.root_dir + -- Merge into client.settings + -- Send workspace/didChangeConfiguration notification end, }) -vim.lsp.enable({ "lua_ls", "intelephense", ... }) ``` **Key decisions**: - No neoconf (incompatible with Neovim 0.11+, issue #116) - Native `exrc` + `secure` for project config (see settings.lua) -- `on_new_config` hook loads from actual `root_dir`, not cwd -- `workspace/didChangeConfiguration` sent in `on_attach` for servers like intelephense +- Project settings loaded via `LspAttach` autocmd (NOT `on_new_config` which is deprecated) +- `workspace/didChangeConfiguration` sent after merging settings into `client.settings` ### 2. Project-Local Configuration **Format**: `.nvim.lua` at project root, returns: diff --git a/README.md b/README.md index 4b2b07e..9992d9d 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Record every decision here with a short rationale. Append new entries; do not re - 2025-12-06: Use `mason.nvim` + `mason-lspconfig.nvim` to install/manage LSP servers; keep custom lspconfig setup; include `.neoconf.json` in root detection. - For PHP validation inside this repo: we require `.neoconf.json` to attach `intelephense` to avoid the repo’s `.git` being chosen as the LSP root. - 2025-12-06: Plugin approval policy — other plugins are allowed, but do not install any plugin not listed without explicit confirmation. +- 2025-12-07: **CRITICAL REGRESSION FIX**: Neovim 0.11+ does NOT support `on_new_config` callback in `vim.lsp.config()`. Project-local settings (`.nvim.lua`) must be loaded via `LspAttach` autocmd instead. The `on_new_config` hook was silently ignored, causing empty settings and breaking goto-definition for external includes. Solution: Use `LspAttach` to load project settings, merge into `client.settings`, and send `workspace/didChangeConfiguration` notification. +- 2025-12-07: Native `exrc` + `.nvim.lua` finalized as project-local config approach (replaces neoconf, which is incompatible with Neovim 0.11+ native LSP API). Security via `vim.opt.secure = true`. ## Project-Local Configuration (design) diff --git a/REGRESSION_FIX_2025-12-07.md b/REGRESSION_FIX_2025-12-07.md new file mode 100644 index 0000000..9a922f2 --- /dev/null +++ b/REGRESSION_FIX_2025-12-07.md @@ -0,0 +1,129 @@ +# Critical Regression Fix: LSP Project Settings Not Loading (2025-12-07) + +## Summary +`gd` (goto definition) stopped working for external library references in `WORKSPACE_TEST/site/external.php`. Even restoring to a git commit where it previously worked didn't fix it, indicating an **environment change** rather than a code regression. + +## Root Cause +**Neovim 0.11+ removed support for the `on_new_config` callback in `vim.lsp.config()`.** + +The previous implementation used `on_new_config` to load project-local settings from `.nvim.lua`: + +```lua +vim.lsp.config(server_name, { + on_new_config = function(new_config, root_dir) + -- Load project settings and merge into new_config.settings + local project_settings = load_project_lsp_settings(server_name, root_dir) + if project_settings.settings then + new_config.settings = vim.tbl_deep_extend("force", + new_config.settings or {}, + project_settings.settings) + end + end, +}) +``` + +**Problem**: This callback was **silently ignored** in Neovim 0.11+, resulting in: +- Empty `client.settings` (confirmed via `:LspInfo` showing `Settings: {}`) +- No `includePaths` sent to intelephense +- `gd` failing with "No locations found" for external library symbols + +## Symptoms +1. `:LspInfo` showed `Settings: {}` for intelephense +2. `gd` on `\ExtLib\Src\Util::greetExternal()` returned "No locations found" +3. Local (workspace) symbol navigation worked fine +4. Git history restore didn't help (environment issue, not code issue) + +## Solution +Use the `LspAttach` autocmd instead of `on_new_config` to load and apply project settings: + +```lua +vim.api.nvim_create_autocmd('LspAttach', { + group = vim.api.nvim_create_augroup('lsp_project_settings', { clear = true }), + callback = function(args) + local client = vim.lsp.get_client_by_id(args.data.client_id) + if not client then return end + + -- Load project-specific settings from .nvim.lua at client.root_dir + local project_settings = load_project_lsp_settings(client.name, client.root_dir) + if project_settings.settings then + -- Merge into client.settings + client.settings = vim.tbl_deep_extend("force", + client.settings or {}, + project_settings.settings) + + -- Notify server of updated settings + vim.schedule(function() + client.notify("workspace/didChangeConfiguration", + { settings = client.settings }) + end) + end + end, +}) +``` + +## Validation +After fix: +```bash +cd WORKSPACE_TEST +nvim --headless +'e site/external.php' +'lua vim.wait(3000); local clients = vim.lsp.get_clients({name="intelephense"}); if clients[1] then print("Settings:", vim.inspect(clients[1].settings)) end' +quit +``` + +Output: +```lua +Settings: { + intelephense = { + environment = { + includePaths = { "/home/ray/.config/nvim/EXTERNAL_TEST" } + } + } +} +``` + +✅ Settings now correctly loaded and `gd` works for external symbols. + +## Key Differences: Neovim 0.10 vs 0.11 LSP API + +### Old API (lspconfig plugin, Neovim < 0.11) +```lua +require('lspconfig').intelephense.setup({ + on_new_config = function(new_config, root_dir) + -- Called when creating new client + end, +}) +``` + +### New API (native vim.lsp.config, Neovim 0.11+) +```lua +-- Define config +vim.lsp.config('intelephense', { ... }) + +-- Enable server +vim.lsp.enable('intelephense') + +-- Handle dynamic settings via autocmd +vim.api.nvim_create_autocmd('LspAttach', { + callback = function(args) + -- Access client, load settings, notify server + end, +}) +``` + +## Documentation Updated +- `README.md`: Added decision log entry for 2025-12-07 +- `.github/copilot-instructions.md`: Documents LspAttach pattern +- `AGENTS.md`: Already notes "Native exrc + vim.lsp.config migration done" + +## Prevention +- Always test LSP functionality after Neovim version upgrades +- Check `:help news` for API changes +- Validate `:LspInfo` settings are populated correctly +- Test with external library references, not just workspace files + +## Files Modified +- `lua/plugins/lsp.lua`: Replaced `on_new_config` with `LspAttach` autocmd + +## Timeline +- **Before 2025-12-07**: `on_new_config` worked in older Neovim version +- **2025-12-07**: Neovim upgraded to 0.11.5, `on_new_config` silently ignored +- **2025-12-07**: Diagnosed via `:LspInfo` showing empty settings +- **2025-12-07**: Fixed with `LspAttach` pattern, verified working diff --git a/lua/plugins/lsp.lua b/lua/plugins/lsp.lua index e075c8a..eebd3a7 100644 --- a/lua/plugins/lsp.lua +++ b/lua/plugins/lsp.lua @@ -9,6 +9,9 @@ return { -- Load project-local LSP settings from .nvim.lua local function load_project_lsp_settings(server_name, root_dir) + if not root_dir then + return {} + end local config_file = root_dir .. "/.nvim.lua" if vim.fn.filereadable(config_file) == 1 then local ok, project_config = pcall(dofile, config_file) @@ -19,14 +22,8 @@ return { return {} end - local function on_attach(client, bufnr) - -- Send settings to server after attach (many servers need didChangeConfiguration) - if client.config.settings and next(client.config.settings) then - vim.schedule(function() - client.notify("workspace/didChangeConfiguration", { settings = client.config.settings }) - end) - end - + -- Setup keymaps on attach + local function setup_keymaps(bufnr) local map = function(mode, lhs, rhs, desc) vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, silent = true, noremap = true, desc = desc }) end @@ -37,6 +34,36 @@ return { map('n', 'K', vim.lsp.buf.hover, 'LSP: Hover') end + -- Global LspAttach handler to load project settings and setup keymaps + vim.api.nvim_create_autocmd('LspAttach', { + group = vim.api.nvim_create_augroup('lsp_project_settings', { clear = true }), + callback = function(args) + local client = vim.lsp.get_client_by_id(args.data.client_id) + if not client then + return + end + + -- Setup keymaps for this buffer + setup_keymaps(args.buf) + + -- Load and send project-specific settings + local project_settings = load_project_lsp_settings(client.name, client.root_dir) + if project_settings.settings then + -- Merge with existing settings + client.settings = vim.tbl_deep_extend( + "force", + client.settings or {}, + project_settings.settings + ) + + -- Send didChangeConfiguration to update the server + vim.schedule(function() + client.notify("workspace/didChangeConfiguration", { settings = client.settings }) + end) + end + end, + }) + -- Configure servers using vim.lsp.config (Neovim 0.11+ API) local function configure(server_name, opts) opts = opts or {} @@ -44,18 +71,6 @@ return { -- Merge with defaults local config = vim.tbl_deep_extend("force", { capabilities = capabilities, - on_attach = on_attach, - on_new_config = function(new_config, root_dir) - -- Load project-specific settings based on actual root_dir - local project_settings = load_project_lsp_settings(server_name, root_dir) - if project_settings.settings then - new_config.settings = vim.tbl_deep_extend( - "force", - new_config.settings or {}, - project_settings.settings - ) - end - end, }, opts) -- Configure the server using the new API