diff --git a/modules/nvim/config/lazy-lock.json b/modules/nvim/config/lazy-lock.json index a5251f8..81c96e2 100644 --- a/modules/nvim/config/lazy-lock.json +++ b/modules/nvim/config/lazy-lock.json @@ -13,7 +13,7 @@ "lualine.nvim": { "branch": "master", "commit": "b8c23159c0161f4b89196f74ee3a6d02cdc3a955" }, "mini.diff": { "branch": "main", "commit": "7077b636d642fcd5cd48554e187a251883239660" }, "mini.icons": { "branch": "main", "commit": "f9a177c11daa7829389b7b6eaaec8b8a5c47052d" }, - "mini.pairs": { "branch": "main", "commit": "6e1cc569130f25b2c6fa16d8b21b31ddb1420a4a" }, + "mini.pairs": { "branch": "main", "commit": "3738ea30ff33e0cbf2983dc67319a5468d25b0a9" }, "none-ls-extras.nvim": { "branch": "main", "commit": "924fe88a9983c7d90dbb31fc4e3129a583ea0a90" }, "none-ls.nvim": { "branch": "main", "commit": "df778107fd2f0503f5606363ce13437132056d54" }, "nvim-surround": { "branch": "main", "commit": "a868c256c861044beb9794b4dd126480dcdfbdad" }, @@ -21,9 +21,9 @@ "nvim-web-devicons": { "branch": "master", "commit": "6e51ca170563330e063720449c21f43e27ca0bc1" }, "oil.nvim": { "branch": "master", "commit": "07f80ad645895af849a597d1cac897059d89b686" }, "plenary.nvim": { "branch": "master", "commit": "b9fd5226c2f76c951fc8ed5923d85e4de065e509" }, - "render-markdown.nvim": { "branch": "main", "commit": "e76eb2e4262f0f0a1a7bd7a454dd7d44f1299afd" }, + "render-markdown.nvim": { "branch": "main", "commit": "95994ad668786cd75d0ee6d0b7178bce7c3ca146" }, "rose-pine": { "branch": "main", "commit": "72a04c4065345b51b56aed4859ea1d884f734097" }, - "schemastore.nvim": { "branch": "main", "commit": "2ae6d27897c60265d4ad3f33e286528d519098fd" }, + "schemastore.nvim": { "branch": "main", "commit": "fb58187b76d8e086f08686b872b50f52eac57818" }, "snacks.nvim": { "branch": "main", "commit": "bc0630e43be5699bb94dadc302c0d21615421d93" }, "substitute.nvim": { "branch": "main", "commit": "9db749a880e3dd3b0eb57f698aa8f1e1630e1f25" }, "todo-comments.nvim": { "branch": "main", "commit": "304a8d204ee787d2544d8bc23cd38d2f929e7cc5" }, diff --git a/modules/nvim/config/lsp/eslint.lua b/modules/nvim/config/lsp/eslint.lua new file mode 100644 index 0000000..53aa960 --- /dev/null +++ b/modules/nvim/config/lsp/eslint.lua @@ -0,0 +1,185 @@ +local utils = require("dnsc.utils") +local lsp = vim.lsp + +local eslint_config_files = { + ".eslintrc", + ".eslintrc.js", + ".eslintrc.cjs", + ".eslintrc.yaml", + ".eslintrc.yml", + ".eslintrc.json", + "eslint.config.js", + "eslint.config.mjs", + "eslint.config.cjs", + "eslint.config.ts", + "eslint.config.mts", + "eslint.config.cts", +} + +---@type vim.lsp.Config +return { + cmd = { "vscode-eslint-language-server", "--stdio" }, + filetypes = { + "javascript", + "javascriptreact", + "javascript.jsx", + "typescript", + "typescriptreact", + "typescript.tsx", + "vue", + "svelte", + "astro", + "htmlangular", + }, + workspace_required = true, + on_attach = function(client, bufnr) + vim.api.nvim_buf_create_user_command(0, "LspEslintFixAll", function() + client:request_sync("workspace/executeCommand", { + command = "eslint.applyAllFixes", + arguments = { + { + uri = vim.uri_from_bufnr(bufnr), + version = lsp.util.buf_versions[bufnr], + }, + }, + }, nil, bufnr) + end, {}) + end, + root_dir = function(bufnr, on_dir) + -- The project root is where the LSP can be started from + -- As stated in the documentation above, this LSP supports monorepos and simple projects. + -- We select then from the project root, which is identified by the presence of a package + -- manager lock file. + local root_markers = { "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "bun.lock", "deno.lock" } + -- Give the root markers equal priority by wrapping them in a table + root_markers = vim.fn.has("nvim-0.11.3") == 1 and { root_markers } or root_markers + local project_root = vim.fs.root(bufnr, root_markers) + if not project_root then + return + end + + -- We know that the buffer is using ESLint if it has a config file + -- in its directory tree. + -- + -- Eslint used to support package.json files as config files, but it doesn't anymore. + -- We keep this for backward compatibility. + local filename = vim.api.nvim_buf_get_name(bufnr) + local eslint_config_files_with_package_json = + utils.insert_package_json(eslint_config_files, "eslintConfig", filename) + local is_buffer_using_eslint = vim.fs.find(eslint_config_files_with_package_json, { + path = filename, + type = "file", + limit = 1, + upward = true, + stop = vim.fs.dirname(project_root), + })[1] + if not is_buffer_using_eslint then + return + end + + on_dir(project_root) + end, + -- Refer to https://github.com/Microsoft/vscode-eslint#settings-options for documentation. + settings = { + validate = "on", + packageManager = nil, + useESLintClass = false, + experimental = { + useFlatConfig = false, + }, + codeActionOnSave = { + enable = false, + mode = "all", + }, + format = true, + quiet = false, + onIgnoredFiles = "off", + rulesCustomizations = {}, + run = "onType", + problems = { + shortenToSingleLine = false, + }, + -- nodePath configures the directory in which the eslint server should start its node_modules resolution. + -- This path is relative to the workspace folder (root dir) of the server instance. + nodePath = "", + -- use the workspace folder location or the file location (if no workspace folder is open) as the working directory + workingDirectory = { mode = "auto" }, + codeAction = { + disableRuleComment = { + enable = true, + location = "separateLine", + }, + showDocumentation = { + enable = true, + }, + }, + }, + before_init = function(_, config) + -- The "workspaceFolder" is a VSCode concept. It limits how far the + -- server will traverse the file system when locating the ESLint config + -- file (e.g., .eslintrc). + local root_dir = config.root_dir + + if root_dir then + config.settings = config.settings or {} + config.settings.workspaceFolder = { + uri = root_dir, + name = vim.fn.fnamemodify(root_dir, ":t"), + } + + -- Support flat config files + -- They contain 'config' in the file name + local flat_config_files = vim.tbl_filter(function(file) + return file:match("config") + end, eslint_config_files) + + for _, file in ipairs(flat_config_files) do + local found_files = vim.fn.globpath(root_dir, file, true, true) + + -- Filter out files inside node_modules + local filtered_files = {} + for _, found_file in ipairs(found_files) do + if string.find(found_file, "[/\\]node_modules[/\\]") == nil then + table.insert(filtered_files, found_file) + end + end + + if #filtered_files > 0 then + config.settings.experimental = config.settings.experimental or {} + config.settings.experimental.useFlatConfig = true + break + end + end + + -- Support Yarn2 (PnP) projects + local pnp_cjs = root_dir .. "/.pnp.cjs" + local pnp_js = root_dir .. "/.pnp.js" + if vim.uv.fs_stat(pnp_cjs) or vim.uv.fs_stat(pnp_js) then + local cmd = config.cmd + config.cmd = vim.list_extend({ "yarn", "exec" }, cmd) + end + end + end, + handlers = { + ["eslint/openDoc"] = function(_, result) + if result then + vim.ui.open(result.url) + end + return {} + end, + ["eslint/confirmESLintExecution"] = function(_, result) + if not result then + return + end + return 4 -- approved + end, + ["eslint/probeFailed"] = function() + vim.notify("[lspconfig] ESLint probe failed.", vim.log.levels.WARN) + return {} + end, + ["eslint/noLibrary"] = function() + vim.notify("[lspconfig] Unable to find ESLint library.", vim.log.levels.WARN) + return {} + end, + }, +} diff --git a/modules/nvim/config/lua/dnsc/lsp.lua b/modules/nvim/config/lua/dnsc/lsp.lua index 530512c..ec0df4e 100644 --- a/modules/nvim/config/lua/dnsc/lsp.lua +++ b/modules/nvim/config/lua/dnsc/lsp.lua @@ -5,6 +5,7 @@ vim.lsp.enable("nil_ls") vim.lsp.enable("astro") vim.lsp.enable("tailwindcss") vim.lsp.enable("gopls") +vim.lsp.enable("eslint") vim.diagnostic.config({ virtual_text = false, diff --git a/modules/nvim/config/lua/dnsc/utils.lua b/modules/nvim/config/lua/dnsc/utils.lua index 434fe5d..d48590b 100644 --- a/modules/nvim/config/lua/dnsc/utils.lua +++ b/modules/nvim/config/lua/dnsc/utils.lua @@ -16,7 +16,7 @@ end function search_ancestors(startpath, func) if nvim_eleven then - validate('func', func, 'function') + validate("func", func, "function") end if func(startpath) then return startpath @@ -36,13 +36,13 @@ function search_ancestors(startpath, func) end local function escape_wildcards(path) - return path:gsub('([%[%]%?%*])', '\\%1') + return path:gsub("([%[%]%?%*])", "\\%1") end function strip_archive_subpath(path) -- Matches regex from zip.vim / tar.vim - path = vim.fn.substitute(path, 'zipfile://\\(.\\{-}\\)::[^\\\\].*$', '\\1', '') - path = vim.fn.substitute(path, 'tarfile:\\(.\\{-}\\)::.*$', '\\1', '') + path = vim.fn.substitute(path, "zipfile://\\(.\\{-}\\)::[^\\\\].*$", "\\1", "") + path = vim.fn.substitute(path, "tarfile:\\(.\\{-}\\)::.*$", "\\1", "") return path end @@ -125,9 +125,32 @@ local function compile_project() vim.cmd("startinsert") end +local function root_markers_with_field(root_files, new_names, field, fname) + local path = vim.fn.fnamemodify(fname, ":h") + local found = vim.fs.find(new_names, { path = path, upward = true }) + + for _, f in ipairs(found or {}) do + -- Match the given `field`. + for line in io.lines(f) do + if line:find(field) then + root_files[#root_files + 1] = vim.fs.basename(f) + break + end + end + end + + return root_files +end + +local function insert_package_json(root_files, field, fname) + return root_markers_with_field(root_files, { "package.json", "package.json5" }, field, fname) +end + return { compile_project = compile_project, filter = filter, filterReactDTS = filterReactDTS, - root_pattern = root_pattern + root_pattern = root_pattern, + insert_package_json = insert_package_json, + root_markers_with_field = root_markers_with_field, } diff --git a/modules/nvim/config/lua/plugins/none-ls.lua b/modules/nvim/config/lua/plugins/none-ls.lua deleted file mode 100644 index 421921c..0000000 --- a/modules/nvim/config/lua/plugins/none-ls.lua +++ /dev/null @@ -1,63 +0,0 @@ -return { - "nvimtools/none-ls.nvim", - dependencies = { - "nvimtools/none-ls-extras.nvim", - }, - config = function() - local null_ls = require("null-ls") - local utils = require("dnsc.utils") - - local string_starts_with = function(str, start) - return string.sub(str, 1, #start) == start - end - - local create_runtime_condition = function(config_names) - local bufnr_cache = {} - local config_path_cache = {} - - return function(params) - if bufnr_cache[params.bufnr] ~= nil then - return bufnr_cache[params.bufnr] - else - for _, cached_config_path in ipairs(config_path_cache) do - if string_starts_with(params.bufname, cached_config_path) then - bufnr_cache[params.bufnr] = true - return true - end - end - end - - local config_path = utils.root_pattern(config_names)(params.bufname) - - local has_config = config_path ~= nil - if has_config then - table.insert(config_path_cache, config_path) - end - bufnr_cache[params.bufnr] = has_config - - return has_config - end - end - - local eslint_runtime_condition = create_runtime_condition({ - ".eslintrc.cjs", - ".eslintrc.js", - ".eslintrc.json", - ".eslintrc.yaml", - ".eslintrc.yml", - "eslint.config.mjs", - "eslint.config.js", - "eslint.config.ts", - "eslint.config.mts", - }) - - null_ls.setup({ - sources = { - require("none-ls.code_actions.eslint_d"), - require("none-ls.diagnostics.eslint_d").with({ - runtime_condition = eslint_runtime_condition, - }), - }, - }) - end, -}