diff --git a/flake.nix b/flake.nix index 348e44b..c32a03a 100644 --- a/flake.nix +++ b/flake.nix @@ -226,59 +226,15 @@ (builtins.filter (n: builtins.substring 0 12 n == "nvim_plugin-") (builtins.attrNames inputs)); }); - # These are appended at the start of the path so that they take precedence over local install tools + # All runtime dependencies are now optional and checked at runtime by plugins + # This keeps the neovim flake lean and allows project devShells to provide tools + # Core dependencies that neovim itself might need can still be added here runtimeDependencies = with pkgs; [ - # tools - ripgrep # search - fd # search - fzf # search fuzzy - tree-sitter - glow # markdown renderer - curl # http requests - sshfs # remote dev for nosduco/remote-sshfs.nvim - # nodePackages.cspell TODO check out `typos` rust checker instead? - ]; - - # These are appended at the end of the PATH so any local installed tools will take precedence - defaultRuntimeDependencies = with pkgs; [ - # linters - markdownlint-cli - biome # (t|s)j[x] - # formatters - stylua - nixfmt-rfc-style - nodePackages.prettier - rustywind - markdownlint-cli2 - sql-formatter - libsForQt5.qt5.qtdeclarative # qmlformat - # LSPs - # python312Packages.tiktoken # needed for copilot chat - nil # nix - lua-language-server - vscode-langservers-extracted # HTML/CSS/JSON/ESLint - nodePackages.typescript-language-server - nodePackages.svelte-language-server - tailwindcss-language-server - python312Packages.python-lsp-server - rust-analyzer - marksman # markdown - taplo # toml - yaml-language-server - lemminx # xml - gopls # go - # ocamlPackages.ocaml-lsp # ocaml - # Other - typescript - nodejs_24 - clang - # zig - (pkgs.rust-bin.stable.latest.default.override { - extensions = [ - "rust-src" - "rust-analyzer" - ]; - }) + # Keeping ripgrep and fd as they're core to telescope functionality + ripgrep # search - used heavily by telescope + fd # file finding - used by telescope + # All other tools (LSPs, formatters, linters, glow, sshfs, etc.) are now optional + # and will show helpful errors when missing ]; in @@ -299,16 +255,12 @@ generatedWrapperArgs = old.generatedWrapperArgs or [ ] ++ [ - # Add runtime dependencies to neovim path - "--prefix" - "PATH" - ":" - "${lib.makeBinPath runtimeDependencies}" - # Some we will suffix so we pick up the local dev shell intead and default to these otherwise + # Add minimal runtime dependencies to neovim path + # Most tools are now optional and checked at runtime "--suffix" "PATH" ":" - "${lib.makeBinPath defaultRuntimeDependencies}" + "${lib.makeBinPath runtimeDependencies}" ] ++ [ # Set the LAZY env path to the nix store, see init.lua for how it is used diff --git a/lua/plugins/conform_formatter.lua b/lua/plugins/conform_formatter.lua index 7694bba..d651548 100644 --- a/lua/plugins/conform_formatter.lua +++ b/lua/plugins/conform_formatter.lua @@ -50,6 +50,30 @@ end return { "stevearc/conform.nvim", + init = function() + -- Check for common formatters and warn if missing + local formatters_to_check = { + { cmd = "stylua", desc = "Lua formatting" }, + { cmd = "nixfmt", desc = "Nix formatting" }, + { cmd = "prettier", desc = "JS/TS/Svelte formatting (alternative: prettierd)" }, + { cmd = "rustywind", desc = "Tailwind class sorting" }, + { cmd = "markdownlint-cli2", desc = "Markdown formatting" }, + { cmd = "sql-formatter", desc = "SQL formatting" }, + { cmd = "rustfmt", desc = "Rust formatting" }, + } + + for _, formatter in ipairs(formatters_to_check) do + if not U.cmd_executable(formatter.cmd) then + -- Only warn once on startup, not on every format attempt + vim.schedule(function() + vim.notify( + string.format("Formatter '%s' not found. Used for: %s", formatter.cmd, formatter.desc), + vim.log.levels.WARN + ) + end) + end + end + end, opts = { -- https://github.com/stevearc/conform.nvim?tab=readme-ov-file#setup notify_on_error = true, diff --git a/lua/plugins/lint.lua b/lua/plugins/lint.lua index 4185f9a..722607c 100644 --- a/lua/plugins/lint.lua +++ b/lua/plugins/lint.lua @@ -2,6 +2,24 @@ return { "mfussenegger/nvim-lint", event = { "VeryLazy", "BufWritePost", "BufReadPost", "InsertLeave" }, + init = function() + -- Check for common linters and warn if missing + local linters_to_check = { + { cmd = "markdownlint", desc = "Markdown linting" }, + { cmd = "biome", desc = "JS/TS linting" }, + } + + for _, linter in ipairs(linters_to_check) do + if not U.cmd_executable(linter.cmd) then + vim.schedule(function() + vim.notify( + string.format("Linter '%s' not found. Used for: %s", linter.cmd, linter.desc), + vim.log.levels.WARN + ) + end) + end + end + end, opts = { -- Event to trigger linters events = { "BufWritePost", "BufReadPost", "InsertLeave", "CursorHold", "CursorHoldI" }, diff --git a/lua/plugins/lsp.lua b/lua/plugins/lsp.lua index c4b0cdb..6392ecb 100644 --- a/lua/plugins/lsp.lua +++ b/lua/plugins/lsp.lua @@ -219,7 +219,16 @@ return { for _, server_name in ipairs(lsp_servers) do local server_opts = servers[server_name] or {} vim.lsp.config(server_name, server_opts) - vim.lsp.enable(server_name) + -- Try to enable LSP and show helpful error if server not found + local ok, err = pcall(function() + vim.lsp.enable(server_name) + end) + if not ok then + vim.notify( + string.format("LSP '%s' failed to start. Install it in your project devShell.\nError: %s", server_name, err), + vim.log.levels.ERROR + ) + end end else -- TODO test this out on a non nix setup... diff --git a/lua/plugins/markdown_glow.lua b/lua/plugins/markdown_glow.lua index dfea216..ff7f8de 100644 --- a/lua/plugins/markdown_glow.lua +++ b/lua/plugins/markdown_glow.lua @@ -1,5 +1,14 @@ return { "lnc3l0t/glow.nvim", + init = function() + -- Check if glow is available + if not U.cmd_executable("glow") then + vim.notify( + "'glow' not found on PATH. Required for markdown preview with :Glow", + vim.log.levels.INFO + ) + end + end, opts = { default_type = "keep", }, diff --git a/lua/plugins/remote-sshfs.lua b/lua/plugins/remote-sshfs.lua index ae93bcf..f0c288d 100644 --- a/lua/plugins/remote-sshfs.lua +++ b/lua/plugins/remote-sshfs.lua @@ -1,5 +1,14 @@ return { "nosduco/remote-sshfs.nvim", + init = function() + -- Check if sshfs is available + if not U.cmd_executable("sshfs") then + vim.notify( + "'sshfs' not found on PATH. Required for RemoteSSHFS commands", + vim.log.levels.INFO + ) + end + end, cmd = { "RemoteSshfs", "RemoteSSHFSConnect", diff --git a/lua/plugins/telescope.lua b/lua/plugins/telescope.lua index cda1219..fcf10c8 100644 --- a/lua/plugins/telescope.lua +++ b/lua/plugins/telescope.lua @@ -7,11 +7,17 @@ return { { "aznhe21/actions-preview.nvim", event = "VeryLazy" }, }, init = function() + -- Check for essential telescope tools U.cmd_executable("rg", { [false] = function() - vim.notify("rg not installed, live grep will not function.", 2) + vim.notify("'rg' (ripgrep) not found. Required for telescope live grep. Install it in your environment.", vim.log.levels.ERROR) end, }) + + -- fd is optional but improves file finding performance + if not U.cmd_executable("fd") then + vim.notify("'fd' not found. Telescope will use 'find' instead (slower). Consider installing fd.", vim.log.levels.INFO) + end end, cmd = "Telescope", opts = function() diff --git a/lua/util.lua b/lua/util.lua index a399d2e..5c19a46 100644 --- a/lua/util.lua +++ b/lua/util.lua @@ -38,6 +38,23 @@ function M.cmd_executable(cmd, callback) return executable end +-- Check if command exists and show helpful error if not +-- @param cmd string: The command to check for +-- @param feature_name string: Human-readable description of what needs this command +-- @param level number: vim.log.levels (ERROR, WARN, INFO, etc.) - defaults to ERROR +-- @return boolean: true if command exists, false otherwise +function M.require_cmd(cmd, feature_name, level) + level = level or vim.log.levels.ERROR + if vim.fn.executable(cmd) ~= 1 then + vim.notify( + string.format("'%s' not found on PATH. Required for: %s", cmd, feature_name), + level + ) + return false + end + return true +end + -- [1]: (string) lhs (required) -- [2]: (string|fun()) rhs (optional) -- mode: (string|string[]) mode (optional, defaults to "n")