jpease/hex-outdated.nvim

github github
dependency-management
stars 2
issues 0
subscribers 0
forks 0
CREATED

UPDATED


hex-outdated.nvim

CI License: MIT Neovim

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.

hex-outdated.nvim showing inline version status in a mix.exs buffer

Features

  • Live inline virtual text as you edit mix.exs — no :command needed
  • Status at a glance: up to date, upgradable, outdated pin, or non-existent
  • Non-existent versions/packages also show up as real vim.diagnostic entries
  • One-key actions: upgrade the requirement under the cursor, browse published versions, or open the package on hex.pm
  • Treesitter parsing, with a Lua-pattern fallback when the elixir parser is missing
  • Status comes from mix.exs alone — no shelling out to mix
  • Async, cached hex.pm requests; configurable text, highlights, and keymaps

Requirements

  • Neovim 0.10+
  • curl on PATH
  • (Recommended) the elixir Treesitter parser (:TSInstall elixir); without it, the plugin falls back to Lua patterns.

Install

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().

Usage

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 meanings

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).

Lock context (optional)

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 press K on a dependency line) shows requirement / locked / latest for the dependency under the cursor.
  • Lens — :HexOutdated lock toggles a locked X · latest Y line under each dependency (off by default).
  • Stale-lock diagnostic — a warning when mix.lock holds a version that no longer satisfies your requirement (e.g. after you tighten it), prompting a mix 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.

Configuration

setup merges your options over the defaults:

require("hex-outdated").setup({
  enabled = true,            -- show status when a mix.exs opens (:HexOutdated toggle flips it per buffer)
  auto_update = true,        -- re-analyze on buffer changes
  debounce_ms = 500,         -- delay (milliseconds) before re-analyzing after an edit
  api = {
    base_url = "https://hex.pm/api",
    timeout_ms = 5000,          -- invalid/non-positive values use 5000
    max_concurrent = 8,        -- cap on simultaneous curl processes
  },
  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
  },
  cache = {
    ttl_seconds = 3600,        -- how long a successful fetch is cached
    error_ttl_seconds = 60,    -- how long a failed fetch is cached before retry
  },
  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",      -- any nvim_open_win() border
    max_height = 20,         -- max lines in the versions/info float
    hover_key = "K",         -- key for the detail float; false leaves K unmapped
  },
  -- buffer-local keymaps, action = lhs; actions are the :HexOutdated
  -- subcommands: refresh, toggle, upgrade, versions, open, info, lock
  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.

How it works

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). For a composed setting such as deps: deps() ++ extra(), only the leading call or list is read. 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.

Development

just check     # fmt-check, lint, test, test-nvim
just test      # busted suite in spec/ (no Neovim)
just test-nvim # nvim --headless -u NONE -i NONE -l test/run.lua
just format    # stylua
just lint      # luacheck

spec/ holds specs that run without Neovim, stubbing the vim APIs a module touches. test/ holds specs that need a real editor (Treesitter, extmarks, diagnostics, jobs, buffers) and runs with only nvim on PATH.

CI runs stylua and luacheck on Ubuntu, busted on Ubuntu and macOS, and the headless suite on Ubuntu, macOS, and Windows. The Ubuntu and macOS legs install the elixir Treesitter parser and set HEX_OUTDATED_REQUIRE_TS=1, so the Treesitter specs fail rather than skip if it is missing.

License

MIT © Justin Pease