diff --git a/claude/.claude/settings.json b/claude/.claude/settings.json new file mode 100644 index 0000000..be47d4a --- /dev/null +++ b/claude/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "lua-lsp@claude-plugins-official": true + } +} diff --git a/nvim/.config/nvim/after/plugin/conform.lua b/nvim/.config/nvim/after/plugin/conform.lua index bbe068b..c733c23 100644 --- a/nvim/.config/nvim/after/plugin/conform.lua +++ b/nvim/.config/nvim/after/plugin/conform.lua @@ -2,17 +2,89 @@ if vim.g.vscode then return end -require("conform").setup({ - format_on_save = { - -- These options will be passed to conform.format() - timeout_ms = 500, - lsp_format = "fallback", +local util = require("conform.util") +local vite_plus = require("greg.vite_plus") + +-- vp fmt evaluates vite.config.ts and applies the fmt block. oxfmt LSP cannot load +-- vite.config.ts files that contain functions (e.g. lazyPlugins). +require("conform").formatters.vp_fmt = { + meta = { + url = "https://viteplus.dev", + description = "Vite+ formatter via vp fmt (vite.config.ts fmt block)", }, + command = util.from_node_modules("vp"), + args = { "fmt", "--stdin-filepath", "$FILENAME" }, + stdin = true, + cwd = function(_, ctx) + return vim.fs.root(ctx.dirname, { "vite.config.ts", "vite.config.mts", "package.json" }) + end, +} + +---@param bufnr integer +---@return boolean +local function is_js_ts_buf(bufnr) + local ft = vim.bo[bufnr].filetype + return ft == "typescript" or ft == "typescriptreact" or ft == "javascript" or ft == "javascriptreact" +end + +---@param bufnr integer +---@return boolean +local function use_vp_fmt(bufnr) + return vite_plus.is_project(bufnr) and is_js_ts_buf(bufnr) +end + +---@param bufnr integer +---@return string|nil project_root +local function js_ts_root(bufnr) + local name = vim.api.nvim_buf_get_name(bufnr) + if name == "" then + return nil + end + return vim.fs.root(name, { "package.json", "prettier.config.js", "prettier.config.mjs", ".prettierrc" }) +end + +---@param root string +---@return boolean +local function has_local_prettier(root) + return vim.fn.executable(root .. "/node_modules/.bin/prettier") == 1 +end + +---@param bufnr integer +---@return conform.FiletypeFormatter +local function js_ts_formatters(bufnr) + if vite_plus.is_project(bufnr) then + return { "vp_fmt", lsp_format = "never", timeout_ms = 5000 } + end + + -- Prefer project prettier (MilkTea, etc.). Global prettierd comes from fnm/npm, not + -- brew — Homebrew prettierd shebangs break once node lives under fnm. + local root = js_ts_root(bufnr) + if root and has_local_prettier(root) then + return { "prettier", lsp_format = "never" } + end + + return { "prettierd", "prettier", lsp_format = "never" } +end + +require("conform").setup({ + -- vp fmt shells out (~100ms+); run async after save so :w does not block the UI. + format_on_save = function(bufnr) + if use_vp_fmt(bufnr) then + return nil + end + return { timeout_ms = 500 } + end, + format_after_save = function(bufnr) + if not use_vp_fmt(bufnr) then + return nil + end + return { timeout_ms = 5000, async = true } + end, formatters_by_ft = { lua = { "stylua" }, - typescript = { "prettierd", "prettier", stop_after_first = true }, - typescriptreact = { "prettierd", "prettier", stop_after_first = true }, - javascript = { "prettierd", "prettier", stop_after_first = true }, - javascriptreact = { "prettierd", "prettier", stop_after_first = true }, + typescript = js_ts_formatters, + typescriptreact = js_ts_formatters, + javascript = js_ts_formatters, + javascriptreact = js_ts_formatters, }, }) diff --git a/nvim/.config/nvim/after/plugin/none-ls.lua b/nvim/.config/nvim/after/plugin/none-ls.lua index d6dca02..c6a5d0f 100644 --- a/nvim/.config/nvim/after/plugin/none-ls.lua +++ b/nvim/.config/nvim/after/plugin/none-ls.lua @@ -4,17 +4,39 @@ end local null_ls = require("null-ls") -null_ls.setup({ - sources = { - null_ls.builtins.formatting.stylua, - null_ls.builtins.completion.spell, - -- requires none-ls-extras.nvim - require("none-ls.diagnostics.eslint_d"), - -- requires none-ls-shellcheck.nvim - require("none-ls-shellcheck.diagnostics"), - require("none-ls-shellcheck.code_actions"), - }, -}) +local eslint_config_files = { + "eslint.config.js", + "eslint.config.mjs", + "eslint.config.cjs", + "eslint.config.ts", + ".eslintrc", + ".eslintrc.js", + ".eslintrc.json", + ".eslintrc.cjs", + ".eslintrc.yaml", + ".eslintrc.yml", +} + +local sources = { + null_ls.builtins.formatting.stylua, + null_ls.builtins.completion.spell, + -- requires none-ls-shellcheck.nvim + require("none-ls-shellcheck.diagnostics"), + require("none-ls-shellcheck.code_actions"), +} + +-- Only run eslint_d when an ESLint config exists at the project root. +-- Avoids JSON-decode errors in Vite+ / oxlint-only repos that have no ESLint config. +table.insert( + sources, + require("none-ls.diagnostics.eslint_d").with({ + condition = function(utils) + return utils.root_has_file(eslint_config_files) + end, + }) +) + +null_ls.setup({ sources = sources }) -- Display code actions. vim.keymap.set("n", "", vim.lsp.buf.code_action, { desc = "Code action" }) diff --git a/nvim/.config/nvim/after/plugin/oxc.lua b/nvim/.config/nvim/after/plugin/oxc.lua new file mode 100644 index 0000000..0797630 --- /dev/null +++ b/nvim/.config/nvim/after/plugin/oxc.lua @@ -0,0 +1,20 @@ +if vim.g.vscode then + return +end + +-- Oxlint via native LSP. oxfmt LSP stays enabled for non-TS assets; Vite+ TS/JS +-- formatting uses conform's vp_fmt formatter (see after/plugin/conform.lua). + +vim.lsp.config("oxlint", { + init_options = { + settings = { + run = "onType", + fixKind = "safe_fix", + }, + }, +}) + +vim.lsp.enable("oxlint") +vim.lsp.enable("oxfmt") + +vim.keymap.set("n", "xl", "LspOxlintFixAll", { desc = "Oxlint fix all" }) diff --git a/nvim/.config/nvim/lua/greg/pack.lua b/nvim/.config/nvim/lua/greg/pack.lua index ae8ffa2..22172ee 100644 --- a/nvim/.config/nvim/lua/greg/pack.lua +++ b/nvim/.config/nvim/lua/greg/pack.lua @@ -31,7 +31,7 @@ vim.pack.add({ { src = gh("williamboman/mason.nvim"), version = "cb8445f8ce85d957416c106b780efd51c6298f89" }, { src = gh("williamboman/mason-lspconfig.nvim"), version = "0c2823e0418f3d9230ff8b201c976e84de1cb401" }, - { src = gh("neovim/nvim-lspconfig"), version = "31026a13eefb20681124706a79fc1df6bf11ab27" }, + { src = gh("neovim/nvim-lspconfig"), version = "bfcc0171a43f22afa61d927ffe9fcb6cb85dc99e" }, { src = gh("hrsh7th/nvim-cmp"), version = "a1d504892f2bc56c2e79b65c6faded2fd21f3eca" }, { src = gh("hrsh7th/cmp-nvim-lsp"), version = "cbc7b02bb99fae35cb42f514762b89b5126651ef" }, { src = gh("L3MON4D3/LuaSnip"), version = "a62e1083a3cfe8b6b206e7d3d33a51091df25357" }, diff --git a/nvim/.config/nvim/lua/greg/vite_plus.lua b/nvim/.config/nvim/lua/greg/vite_plus.lua new file mode 100644 index 0000000..b933a5a --- /dev/null +++ b/nvim/.config/nvim/lua/greg/vite_plus.lua @@ -0,0 +1,47 @@ +local M = {} + +---@param bufnr integer +---@return boolean +function M.is_project(bufnr) + local name = vim.api.nvim_buf_get_name(bufnr) + if name == "" then + return false + end + + local root = vim.fs.root(name, { "vite.config.ts", "vite.config.mts", "package.json" }) + if not root then + return false + end + + local pkg_path = root .. "/package.json" + if vim.fn.filereadable(pkg_path) == 1 then + local ok, data = pcall(vim.json.decode, table.concat(vim.fn.readfile(pkg_path), "\n")) + if ok and type(data) == "table" then + for _, key in ipairs({ "dependencies", "devDependencies" }) do + local deps = data[key] + if type(deps) == "table" and deps["vite-plus"] then + return true + end + end + local overrides = data.overrides + if type(overrides) == "table" and type(overrides.vite) == "string" and overrides.vite:find("vite%-plus") then + return true + end + end + end + + for _, filename in ipairs({ "vite.config.ts", "vite.config.mts" }) do + local vite_config = root .. "/" .. filename + if vim.fn.filereadable(vite_config) == 1 then + for _, line in ipairs(vim.fn.readfile(vite_config)) do + if line:find("vite%-plus") then + return true + end + end + end + end + + return false +end + +return M diff --git a/nvim/.config/nvim/nvim-pack-lock.json b/nvim/.config/nvim/nvim-pack-lock.json index e62d42d..298533d 100644 --- a/nvim/.config/nvim/nvim-pack-lock.json +++ b/nvim/.config/nvim/nvim-pack-lock.json @@ -86,9 +86,9 @@ "version": "'a1d504892f2bc56c2e79b65c6faded2fd21f3eca'" }, "nvim-lspconfig": { - "rev": "31026a13eefb20681124706a79fc1df6bf11ab27", + "rev": "bfcc0171a43f22afa61d927ffe9fcb6cb85dc99e", "src": "https://github.com/neovim/nvim-lspconfig", - "version": "'31026a13eefb20681124706a79fc1df6bf11ab27'" + "version": "'bfcc0171a43f22afa61d927ffe9fcb6cb85dc99e'" }, "nvim-transparent": { "rev": "8ac59883de84e9cd1850ea25cf087031c5ba7d54", diff --git a/scripts/brews.sh b/scripts/brews.sh index fdb60ba..7a2739e 100755 --- a/scripts/brews.sh +++ b/scripts/brews.sh @@ -190,7 +190,7 @@ install_python_dev_packages() { # JavaScript/Node.js developement. install_javascript_packages() { - prompt_and_install_shell "JavaScript packages" 'brew install fnm oven-sh/bun/bun pnpm fsouza/prettierd/prettierd && fnm install 22 && npm install -g eslint_d typescript-language-server typescript' + prompt_and_install_shell "JavaScript packages" 'brew install fnm oven-sh/bun/bun pnpm && fnm install --lts && fnm default lts-latest && eval "$(fnm env)" && npm install -g @fsouza/prettierd eslint_d@15 typescript-language-server typescript' } # Gleam. diff --git a/scripts/pacs.sh b/scripts/pacs.sh index 693bfc3..f836054 100755 --- a/scripts/pacs.sh +++ b/scripts/pacs.sh @@ -112,8 +112,10 @@ install_lua_packages() { install_javascript_packages() { prompt_and_install "JavaScript packages" 'pacman -S nodejs npm &&\ npm install -g fnm &&\ - fnm install 22 &&\ - npm install -g eslint_d\ + fnm install --lts &&\ + fnm default lts-latest &&\ + eval "$(fnm env)" &&\ + npm install -g @fsouza/prettierd eslint_d@15\ typescript-language-server\ typescript' }