diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md index 72081b6..aa789ee 100644 --- a/MIGRATION_PLAN.md +++ b/MIGRATION_PLAN.md @@ -260,13 +260,50 @@ Source of truth for the step-by-step rebuild. Keep this concise and up to date. ## Phase 9 — Formatting & Linting ## Phase 9.1 — Confirm scope and priorities -- [ ] Confirm scope and priorities for this phase +- [x] Confirm scope and priorities for this phase +- [x] Decision: Formatters - prettier (JS/TS/CSS/JSON/MD/HTML), phpcbf (PHP), stylua (Lua) +- [x] Decision: Linters - eslint (JS/TS), phpcs (PHP), markdownlint (Markdown) +- [x] Decision: Project-local executables preferred, fallback to global (node_modules/.bin/, vendor/bin/) +- [x] Decision: phpcs/phpcbf already installed globally with WordPress coding standards - no installation needed +- [x] Decision: Format-on-save enabled by default with toggle capability +- [x] Decision: Manual format keymaps: `lf` (normal), `lf` (visual range) +- [x] Decision: Respect project config files (.prettierrc, phpcs.xml, .eslintrc, etc.) +- [x] Strategy: Helper function detects project-local executables first, then global +- [x] WordPress: phpcs.xml in project root or --standard=WordPress flag for WordPress projects +- [x] Philosophy: Formatters are authoritative source of truth; Neovim settings should match formatter rules per filetype +- [x] Note: Phase 9.4 will align Neovim editor settings (tabstop, shiftwidth, expandtab) with formatter configurations -## Phase 9.2 — none-ls setup -- [ ] Add `nvimtools/none-ls.nvim` -- [ ] Configure formatters (to be determined in 9.1) -- [ ] Configure linters (to be determined in 9.1) -- [ ] Add format-on-save or manual format keymaps +## Phase 9.2 — none-ls setup with project-aware executables +- [x] Add `nvimtools/none-ls.nvim` +- [x] Create helper function to detect project-local executables (node_modules/.bin/, vendor/bin/, Mason bin) +- [x] Configure formatters: + - [x] prettier (project-local first, then Mason, then global) + - [x] phpcbf (project-local first, then global system install - already available) + - [x] stylua (Mason installed) +- [x] Add `mfussenegger/nvim-lint` for linting (none-ls removed most linters from builtins) +- [x] Configure linters via nvim-lint: + - [x] eslint_d (project-local first, then Mason, then global - daemon version for speed) + - [x] phpcs (project-local first, then global system install - already available) + - [x] markdownlint (Mason installed) +- [x] Add format-on-save autocommand with toggle capability (`lt` to toggle) +- [x] Add manual format keymaps: `lf` (buffer), `lf` (visual range) +- [x] Ensure WordPress coding standards work via phpcs.xml or --standard flag +- [x] Search order: project node_modules/.bin/ → project vendor/bin/ → Mason bin → system PATH +- [x] Note: Linting runs on BufEnter, BufWritePost, InsertLeave events + +## Phase 9.3 — Mason formatter/linter installation +- [x] Add `WhoIsSethDaniel/mason-tool-installer.nvim` for automated installation +- [x] Install via Mason: prettier, eslint_d, markdownlint, stylua +- [x] Note: phpcs/phpcbf already installed globally, skip Mason installation +- [x] Verify all tools available: Tools installed to ~/.local/share/nvim/mason/bin/ + +## Phase 9.4 — Align Neovim settings with formatter rules +- [ ] Configure per-filetype settings to match formatter defaults +- [ ] PHP: Match phpcs/WordPress standards (likely tabs, 4-width) +- [ ] JS/TS/CSS/JSON: Match prettier defaults (likely 2 spaces) +- [ ] Lua: Match stylua defaults (likely tabs or 4 spaces) +- [ ] Use autocmds or after/ftplugin/ for filetype-specific tabstop, shiftwidth, expandtab +- [ ] Goal: Manual editing feels natural and matches what formatter will produce on save ## Phase 10 — Migrate Kept Behaviours diff --git a/lazy-lock.json b/lazy-lock.json index d7f4182..98a2d44 100644 --- a/lazy-lock.json +++ b/lazy-lock.json @@ -10,9 +10,12 @@ "indent-blankline.nvim": { "branch": "master", "commit": "005b56001b2cb30bfa61b7986bc50657816ba4ba" }, "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, "mason-lspconfig.nvim": { "branch": "main", "commit": "0b9bb925c000ae649ff7e7149c8cd00031f4b539" }, + "mason-tool-installer.nvim": { "branch": "main", "commit": "517ef5994ef9d6b738322664d5fdd948f0fdeb46" }, "mason.nvim": { "branch": "main", "commit": "57e5a8addb8c71fb063ee4acda466c7cf6ad2800" }, + "none-ls.nvim": { "branch": "main", "commit": "5abf61927023ea83031753504adb19630ba80eef" }, "nvim-autopairs": { "branch": "master", "commit": "7a2c97cccd60abc559344042fefb1d5a85b3e33b" }, "nvim-cmp": { "branch": "main", "commit": "d97d85e01339f01b842e6ec1502f639b080cb0fc" }, + "nvim-lint": { "branch": "master", "commit": "ebe535956106c60405b02220246e135910f6853d" }, "nvim-lspconfig": { "branch": "master", "commit": "9c923997123ff9071198ea3b594d4c1931fab169" }, "nvim-surround": { "branch": "main", "commit": "fcfa7e02323d57bfacc3a141f8a74498e1522064" }, "nvim-treesitter": { "branch": "master", "commit": "42fc28ba918343ebfd5565147a42a26580579482" }, diff --git a/lua/plugins/mason-tool-installer.lua b/lua/plugins/mason-tool-installer.lua new file mode 100644 index 0000000..d107406 --- /dev/null +++ b/lua/plugins/mason-tool-installer.lua @@ -0,0 +1,24 @@ +-- Mason tool installer for formatters and linters +-- Note: phpcs/phpcbf already installed globally, not included here + +return { + "WhoIsSethDaniel/mason-tool-installer.nvim", + dependencies = { "williamboman/mason.nvim" }, + config = function() + require("mason-tool-installer").setup({ + ensure_installed = { + -- Formatters + "prettier", -- JS, TS, CSS, JSON, Markdown, HTML + "stylua", -- Lua + -- Note: phpcbf already installed globally + + -- Linters + "eslint_d", -- JS, TS (daemon version for speed) + "markdownlint", -- Markdown + -- Note: phpcs already installed globally + }, + auto_update = false, + run_on_start = true, + }) + end, +} diff --git a/lua/plugins/none-ls.lua b/lua/plugins/none-ls.lua new file mode 100644 index 0000000..7466111 --- /dev/null +++ b/lua/plugins/none-ls.lua @@ -0,0 +1,118 @@ +-- 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 + + 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 CodeSniffer (phpcbf) - WordPress standards + formatting.phpcbf.with({ + command = find_executable({ "phpcbf" }), + prefer_local = "vendor/bin", + -- Respects phpcs.xml in project root or uses WordPress standard + extra_args = function() + local phpcs_xml = vim.fn.findfile("phpcs.xml", ".;") + if phpcs_xml == "" then + phpcs_xml = vim.fn.findfile("phpcs.xml.dist", ".;") + end + if phpcs_xml == "" then + return { "--standard=WordPress" } + end + return {} + end, + }), + + -- 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 + vim.lsp.buf.format({ bufnr = bufnr }) + 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 = "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 = "Format buffer", silent = true, noremap = true }) + + -- Manual format (visual range) + vim.keymap.set("v", "lf", function() + vim.lsp.buf.format({ async = false }) + end, { desc = "Format selection", silent = true, noremap = true }) + end, +} diff --git a/lua/plugins/nvim-lint.lua b/lua/plugins/nvim-lint.lua new file mode 100644 index 0000000..db94efa --- /dev/null +++ b/lua/plugins/nvim-lint.lua @@ -0,0 +1,80 @@ +-- nvim-lint: Linting support (replaces none-ls diagnostics) +-- none-ls removed ESLint and many linters, so we use nvim-lint instead + +return { + "mfussenegger/nvim-lint", + event = { "BufReadPre", "BufNewFile" }, + config = function() + local lint = require("lint") + + -- Configure linters per filetype + lint.linters_by_ft = { + javascript = { "eslint_d" }, + javascriptreact = { "eslint_d" }, + typescript = { "eslint_d" }, + typescriptreact = { "eslint_d" }, + markdown = { "markdownlint" }, + php = { "phpcs" }, + } + + -- Helper: Find project-local executable, fallback to global + local function find_executable(names) + local cwd = vim.fn.getcwd() + local mason_bin = vim.fn.stdpath("data") .. "/mason/bin/" + + 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 + + if vim.fn.executable(name) == 1 then + return name + end + end + + return nil + end + + -- Configure phpcs for WordPress standards + lint.linters.phpcs.cmd = find_executable({ "phpcs" }) or "phpcs" + lint.linters.phpcs.args = { + "-q", + "--report=json", + function() + local phpcs_xml = vim.fn.findfile("phpcs.xml", ".;") + if phpcs_xml == "" then + phpcs_xml = vim.fn.findfile("phpcs.xml.dist", ".;") + end + if phpcs_xml == "" then + return "--standard=WordPress" + end + return nil + end, + "-", -- stdin + } + + -- Configure eslint_d to use project-local first + lint.linters.eslint_d.cmd = find_executable({ "eslint_d", "eslint" }) or "eslint_d" + + -- Configure markdownlint + lint.linters.markdownlint.cmd = find_executable({ "markdownlint" }) or "markdownlint" + + -- Auto-lint on these events + local lint_augroup = vim.api.nvim_create_augroup("lint", { clear = true }) + vim.api.nvim_create_autocmd({ "BufEnter", "BufWritePost", "InsertLeave" }, { + group = lint_augroup, + callback = function() + lint.try_lint() + end, + }) + end, +}