Live "are my Hex deps up to date?" feedback for Elixir mix.exs, in the spirit
of crates.nvim. As you edit mix.exs, the plugin checks each dependency's
declared version requirement against hex.pm: inline virtual text shows the
latest version and status, and non-existent versions/packages show up as
diagnostics.
- Live inline virtual text as you edit
mix.exs— no:commandneeded - Status at a glance: up to date, upgradable, outdated pin, or non-existent
- Non-existent versions/packages also show up as real
vim.diagnosticentries - One-key actions: upgrade the requirement under the cursor, browse published versions, or open the package on hex.pm
- Treesitter parsing with a dependency-free Lua-pattern fallback
- Status comes from
mix.exsalone — no shelling out tomix - Async, cached hex.pm requests; configurable text, highlights, and keymaps
- Neovim 0.10+
curlonPATH- (Recommended) the
elixirTreesitter parser (:TSInstall elixir); without it, the plugin falls back to Lua patterns.
With lazy.nvim:
{
"jpease/hex-outdated.nvim",
ft = "elixir",
opts = {},
}With packer.nvim:
use({
"jpease/hex-outdated.nvim",
config = function()
require("hex-outdated").setup({})
end,
})With mini.deps:
MiniDeps.add({ source = "jpease/hex-outdated.nvim" })
require("hex-outdated").setup({})With vim-plug:
Plug 'jpease/hex-outdated.nvim'
" after plug#end():
lua require("hex-outdated").setup({})As a native package (no plugin manager):
git clone https://github.com/jpease/hex-outdated.nvim \
~/.config/nvim/pack/plugins/start/hex-outdated.nvim-- in your init.lua
require("hex-outdated").setup({})Every manager other than lazy.nvim's opts = {} needs an explicit
require("hex-outdated").setup({}) call — the plugin activates entirely through
setup().
Open a mix.exs — status appears automatically and updates as you type.
:HexOutdated {refresh|toggle|upgrade|versions|open|info|lock}
(bare :HexOutdated = refresh)
| Subcommand | Action |
|---|---|
refresh |
Re-fetch, bypassing the cache. |
toggle |
Enable/disable the inline display for the current buffer. |
upgrade |
Rewrite the requirement under the cursor to the latest published version. |
versions |
Floating window of active published versions; <CR> inserts the selected one, q/<Esc> closes. |
open |
Open the package's page on hex.pm in a browser. |
info |
Floating detail view (requirement / locked / latest) for the dependency under the cursor. |
lock |
Toggle the per-buffer lock lens — a locked X · latest Y line under each dependency. |
The same actions are also plain functions, so you can bind your own keys:
local hex = require("hex-outdated")
-- hex.refresh() / hex.toggle() / hex.upgrade() / hex.versions() / hex.open()
-- hex.info() / hex.lock()Selecting a release from versions preserves the current requirement
operator: comparison requirements stay comparisons, bare exact versions stay
bare, and ~> keeps its existing precision (with full prerelease versions
preserved). The plugin excludes retired Hex releases from status calculations
and the popup.
| Status | Meaning |
|---|---|
up_to_date |
The requirement already allows the latest stable release. |
upgradable |
A newer minor/major exists than your requirement targets. |
outdated |
An exact pin (==) that is below the latest release. |
invalid |
No published version matches the requirement (also a diagnostic). |
The plugin leaves Git/path deps unannotated, along with requirements it can't
analyze (combined and/or clauses).
By default hex-outdated reads only mix.exs. When a mix.lock is present it can
also show what's actually locked — kept secondary so the inline requirement
status stays primary:
- Detail float —
:HexOutdated info(or pressKon a dependency line) shows requirement / locked / latest for the dependency under the cursor. - Lens —
:HexOutdated locktoggles alocked X · latest Yline under each dependency (off by default). - Stale-lock diagnostic — a warning when
mix.lockholds a version that no longer satisfies your requirement (e.g. after you tighten it), prompting amix deps.get.
All of this no-ops when there is no mix.lock. Disable it entirely with
lock = { enabled = false }. The K binding only acts on dependency lines and
otherwise falls through to LSP hover / keywordprg; set popup.hover_key = false
to leave K alone.
setup merges your options over the defaults:
require("hex-outdated").setup({
enabled = true,
auto_update = true, -- re-analyze on buffer changes
debounce_ms = 500,
api = {
base_url = "https://hex.pm/api",
timeout_ms = 5000, -- invalid/non-positive values use 5000
max_concurrent = 8, -- cap on simultaneous curl processes
},
cache = {
ttl_seconds = 3600,
error_ttl_seconds = 60, -- how long a failed fetch is cached before retry
},
lock = {
enabled = true, -- read mix.lock when present
lens = false, -- locked-version lens; off by default
stale_diagnostic = true, -- warn when the lock no longer satisfies the requirement
},
text = { -- %s is the latest version
up_to_date = "✓ %s",
upgradable = "↑ %s",
outdated = "↓ %s",
invalid = "✗ no such version",
loading = "…",
error = "fetch error",
lock_behind = "locked %s · latest %s", -- lens line when the lock is behind
lock_current = "locked %s · up to date",
},
highlight = { -- highlight group per status
up_to_date = "HexOutdatedUpToDate",
upgradable = "HexOutdatedUpgradable",
outdated = "HexOutdatedOutdated",
invalid = "HexOutdatedInvalid",
loading = "HexOutdatedLoading",
error = "HexOutdatedError",
lock = "HexOutdatedLock",
lock_behind = "HexOutdatedLockBehind",
},
popup = { border = "rounded", max_height = 20, hover_key = "K" }, -- hover_key=false disables auto-K
-- opt-in buffer-local keymaps (unset by default):
keymaps = {}, -- e.g. { upgrade = "<leader>cu", versions = "<leader>cv", info = "<leader>ci" }
})Highlight groups link to Diagnostic* by default and respect your colorscheme
if you define them first: HexOutdatedUpToDate, HexOutdatedUpgradable,
HexOutdatedOutdated, HexOutdatedInvalid, HexOutdatedLoading,
HexOutdatedError, HexOutdatedLock, HexOutdatedLockBehind.
hex-outdated parses mix.exs with Treesitter (Lua-pattern fallback) to find
dependency tuples inside the function referenced by the project's deps:
setting (deps/0 by default). A Hex package alias such as
{:local_app, "~> 2.0", hex: :actual_package} points the API lookup at the
real package, while the local application name still keys into mix.lock. For
each Hex dependency the plugin asynchronously queries
https://hex.pm/api/packages/:name (cached), excludes retired releases, then
compares your requirement against the active published versions using Hex
prerelease semantics. The requirement status comes from mix.exs alone; the
plugin reads mix.lock locally, and only for the optional lock context above.
There is no shelling out to mix.
just check # stylua --check, luacheck, busted, headless-nvim suite
just test # busted (pure logic, no Neovim)
just test-nvim # headless-Neovim integration suite
just format # stylua
just lint # luacheck
The split is by what a test needs to run. spec/ is the busted suite: anything
that runs without Neovim, stubbing the vim APIs a module touches. test/ is
everything that needs a real editor — Treesitter parsing, extmark/diagnostic
rendering, the curl queue, and buffer actions — run against a headless Neovim
with nvim --headless -u NONE -l test/run.lua (no busted/luarocks needed).
Both suites run in CI.
CI runs the Lua checks on Ubuntu and macOS, and the dependency-free headless-Neovim integration suite on Ubuntu, macOS, and Windows. CI installs the Elixir Treesitter parser best-effort on Linux; parser-independent and platform-sensitive regressions run on every integration platform.
MIT © Justin Pease
