-- none-ls.nvim: Formatting only (linting moved to nvim-lint) -- Philosophy: Formatters are authoritative. Project-local executables preferred. -- Note: none-ls removed most linters from builtins, so we use nvim-lint for diagnostics return { "nvimtools/none-ls.nvim", dependencies = { "nvim-lua/plenary.nvim" }, event = { "BufReadPre", "BufNewFile" }, config = function() local null_ls = require("null-ls") local augroup = vim.api.nvim_create_augroup("LspFormatting", {}) -- Helper: Find project-local executable, fallback to global -- Searches node_modules/.bin/, vendor/bin/, and Mason bin first local function find_executable(names) local cwd = vim.fn.getcwd() local mason_bin = vim.fn.stdpath("data") .. "/mason/bin/" -- Try project-local paths first, then Mason, then global local search_paths = { cwd .. "/node_modules/.bin/", cwd .. "/vendor/bin/", mason_bin, } for _, name in ipairs(names) do for _, path in ipairs(search_paths) do local full_path = path .. name if vim.fn.executable(full_path) == 1 then return full_path end end -- Fallback to system PATH if vim.fn.executable(name) == 1 then return name end end return nil end -- Formatters local formatting = null_ls.builtins.formatting -- Note: Diagnostics (linters) moved to nvim-lint plugin -- Note: Python formatting handled by ruff LSP null_ls.setup({ sources = { -- Prettier (JS, TS, CSS, SCSS, JSON, Markdown, HTML) formatting.prettier.with({ command = find_executable({ "prettier" }), prefer_local = "node_modules/.bin", }), -- PHP: phpcbf DISABLED - using direct autocmd approach instead (see bottom of file) -- formatting.phpcbf causes blank line bug even with custom formatters -- stylua (Lua) formatting.stylua.with({ command = find_executable({ "stylua" }), }), }, -- Format on save on_attach = function(client, bufnr) if client.supports_method("textDocument/formatting") then vim.api.nvim_clear_autocmds({ group = augroup, buffer = bufnr }) vim.api.nvim_create_autocmd("BufWritePre", { group = augroup, buffer = bufnr, callback = function() -- Only format if format-on-save is enabled (global flag) if vim.g.format_on_save ~= false then -- Save view (cursor position, folds, etc.) before formatting local view = vim.fn.winsaveview() vim.lsp.buf.format({ bufnr = bufnr }) -- Restore view after formatting to preserve folds vim.fn.winrestview(view) end end, }) end end, }) -- Format-on-save is enabled by default vim.g.format_on_save = true -- Keymaps -- Toggle format-on-save vim.keymap.set("n", "lt", function() vim.g.format_on_save = not vim.g.format_on_save local status = vim.g.format_on_save and "enabled" or "disabled" vim.notify("Format on save " .. status, vim.log.levels.INFO) end, { desc = "Formatting: Toggle format on save", silent = true, noremap = true }) -- Manual format (buffer) vim.keymap.set("n", "lf", function() vim.lsp.buf.format({ async = false }) end, { desc = "Formatting: Format buffer", silent = true, noremap = true }) -- Manual format (visual range) vim.keymap.set("v", "lf", function() vim.lsp.buf.format({ async = false }) end, { desc = "Formatting: Format selection", silent = true, noremap = true }) -- PHP: Direct phpcbf formatting (bypasses none-ls entirely) vim.api.nvim_create_autocmd("BufWritePre", { pattern = "*.php", callback = function() if vim.g.format_on_save == false then return end local bufnr = vim.api.nvim_get_current_buf() local filepath = vim.api.nvim_buf_get_name(bufnr) -- Find phpcbf local phpcbf = find_executable({ "phpcbf" }) if not phpcbf then return end -- Determine standard local root = vim.fn.getcwd() local has_project_ruleset = vim.loop.fs_stat(root .. "/phpcs.xml") or vim.loop.fs_stat(root .. "/phpcs.xml.dist") local cmd = { phpcbf, "-q", "--stdin-path=" .. filepath } if not has_project_ruleset then table.insert(cmd, "--standard=WordPress") end table.insert(cmd, "-") -- Get buffer content local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local input = table.concat(lines, "\n") -- Run phpcbf local result = vim.fn.system(cmd, input) local exit_code = vim.v.shell_error -- Apply result if successful (exit code 0 or 1) if exit_code == 0 or exit_code == 1 then local output_lines = vim.split(result, "\n", { plain = true }) -- Remove trailing empty line if present if output_lines[#output_lines] == "" then table.remove(output_lines) end vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, output_lines) end end, }) end, }