Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 67 additions & 3 deletions modules/neovim.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# Neovim: install latest stable release from GitHub (prebuilt tarball).
# Falls back to apt if GitHub is unreachable or arch is unsupported.
# Falls back to apt if GitHub is unreachable, arch is unsupported, or the
# prebuilt binary is incompatible with the system's glibc.
# Idempotent: skips install when the installed version already matches latest.

# Probe ~/.local/bin/nvim and ~/bin/nvim for older copies that would shadow
Expand Down Expand Up @@ -92,11 +93,12 @@ install_neovim() {
local prefix
if $CAN_SUDO; then prefix="/usr/local"; else prefix="$HOME/.local"; fi

# Skip if already at latest version
# Skip if already at latest version AND binary actually runs.
# A version-matching but glibc-broken binary must not be skipped.
if has nvim; then
local current
current=$(nvim --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [ "$current" = "$latest" ]; then
if [ "$current" = "$latest" ] && "$prefix/bin/nvim" --version >/dev/null 2>&1; then
log_ok "neovim $latest_tag already installed — skipping"
# Shadow check still needed: `nvim` above may have resolved to a
# user-local copy (e.g. ~/.local/bin/nvim) that shadows an existing
Expand All @@ -110,6 +112,29 @@ install_neovim() {
log_info "neovim: installing $latest_tag → $prefix"
fi

# Early glibc check — avoids a needless ~100 MB download on old systems.
# Prebuilt binaries since v0.10.0 require glibc ≥ 2.32 (Ubuntu 22.04+).
# Detect before downloading; if too old, go straight to the legacy binary.
local glibc_ver
glibc_ver=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || echo "0.0")
if _ver_older_than "$glibc_ver" "2.32"; then
log_warn "neovim: system glibc $glibc_ver < 2.32 — latest prebuilt incompatible"
# Remove any broken binary left by a prior failed install.
if [ -f "$prefix/bin/nvim" ] && ! "$prefix/bin/nvim" --version >/dev/null 2>&1; then
rm -f "$prefix/bin/nvim"
log_info "neovim: removed incompatible binary from $prefix/bin/nvim"
fi
# Keep any already-working compatible binary (e.g. v0.9.5 from a prior run).
if "$prefix/bin/nvim" --version >/dev/null 2>&1; then
local legacy_cur
legacy_cur=$("$prefix/bin/nvim" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
log_ok "neovim $legacy_cur already installed (glibc-compatible) — skipping"
return
fi
[ "$arch" = "x86_64" ] && _neovim_legacy_binary "$prefix" || _neovim_apt
return
fi

if ! $CAN_SUDO; then mkdir -p "$prefix"; fi

local tmp
Expand Down Expand Up @@ -149,9 +174,48 @@ install_neovim() {
if $CAN_SUDO; then _nvim_warn_shadows /usr/local/bin/nvim; fi
}

# Download the last neovim release compatible with glibc < 2.32.
# v0.9.5 was built on Ubuntu 18.04 CI (glibc 2.17 baseline) and runs on any
# glibc ≥ 2.17. Asset name changed to nvim-linux-x86_64 at v0.10.0; v0.9.x
# used nvim-linux64. Only x86_64 is handled — ARM64 falls back to apt.
_neovim_legacy_binary() {
local prefix="$1"
local tag="v0.9.5"
local url="https://github.com/neovim/neovim/releases/download/${tag}/nvim-linux64.tar.gz"
log_info "neovim: downloading legacy $tag (glibc 2.17+ compatible)"

local tmp
tmp=$(mktemp -d)
# shellcheck disable=SC2064
trap "rm -rf '$tmp'" RETURN

if has curl; then
curl -sfL "$url" | tar -xz -C "$tmp" || { log_warn "neovim: legacy download failed — falling back to apt"; _neovim_apt; return; }
else
wget -qO- "$url" | tar -xz -C "$tmp" || { log_warn "neovim: legacy download failed — falling back to apt"; _neovim_apt; return; }
fi

local extracted="$tmp/nvim-linux64"
if [ ! -d "$extracted" ] || ! "$extracted/bin/nvim" --version >/dev/null 2>&1; then
log_warn "neovim: legacy binary not usable — falling back to apt"
_neovim_apt
return
fi

if $CAN_SUDO; then
[ -n "${SUDO:-}" ] && sudo -v 2>/dev/null || true
$SUDO cp -r "$extracted"/. "$prefix/"
else
cp -r "$extracted"/. "$prefix/"
fi
log_ok "neovim legacy $tag installed → $prefix ($($prefix/bin/nvim --version 2>/dev/null | head -1))"
}

_neovim_apt() {
if ! $CAN_SUDO; then
log_warn "neovim: no sudo — cannot install via apt"
log_warn " Prebuilt GitHub binaries require glibc ≥ 2.32 (Ubuntu 22.04+)"
log_warn " Options: upgrade OS, or build neovim from source"
return
fi
apt_install neovim
Expand Down
106 changes: 68 additions & 38 deletions nvim/.config/nvim/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,27 @@ require('lazy').setup({
opts = { style = 'palenight' } }, -- darker/dark/palenight/oceanic

-- ── LSP ──────────────────────────────────────────────────────────────────
-- blink.cmp is a dependency so capabilities are available in config
-- nvim-lspconfig ≥ 2024-12 requires nvim 0.10 at the plugin level (not just
-- API level), so gate the entire block. On nvim 0.9 the editor still works
-- fully; only LSP/completion is absent.
{
'neovim/nvim-lspconfig',
lazy = false,
cond = vim.fn.has('nvim-0.10') == 1,
lazy = false,
dependencies = {
'williamboman/mason.nvim',
'williamboman/mason-lspconfig.nvim',
'saghen/blink.cmp',
},
config = function()
require('mason').setup()
require('mason-lspconfig').setup({
ensure_installed = { 'pyright', 'clangd', 'bashls', 'lua_ls' },
automatic_enable = false, -- we call vim.lsp.enable() ourselves below
})

local capabilities = require('blink.cmp').get_lsp_capabilities()
-- blink.cmp requires nvim ≥ 0.10; fall back to plain capabilities on older.
local capabilities = vim.fn.has('nvim-0.10') == 1
and require('blink.cmp').get_lsp_capabilities()
or vim.lsp.protocol.make_client_capabilities()

-- Single LspAttach autocmd covers all servers — no per-server on_attach needed
-- Single LspAttach autocmd covers all servers — no per-server on_attach needed.
vim.api.nvim_create_autocmd('LspAttach', {
group = vim.api.nvim_create_augroup('UserLspAttach', { clear = true }),
callback = function(event)
Expand All @@ -101,16 +103,22 @@ require('lazy').setup({
local map = function(key, fn, desc)
vim.keymap.set('n', key, fn, vim.tbl_extend('force', opts, { desc = desc }))
end
map('gd', vim.lsp.buf.definition, 'Go to definition')
map('gD', vim.lsp.buf.declaration, 'Go to declaration')
map('gr', vim.lsp.buf.references, 'References')
map('gi', vim.lsp.buf.implementation, 'Go to implementation')
map('K', vim.lsp.buf.hover, 'Hover docs')
map('<leader>rn', vim.lsp.buf.rename, 'Rename symbol')
map('gd', vim.lsp.buf.definition, 'Go to definition')
map('gD', vim.lsp.buf.declaration, 'Go to declaration')
map('gr', vim.lsp.buf.references, 'References')
map('gi', vim.lsp.buf.implementation, 'Go to implementation')
map('K', vim.lsp.buf.hover, 'Hover docs')
map('<leader>rn', vim.lsp.buf.rename, 'Rename symbol')
map('<leader>ca', vim.lsp.buf.code_action, 'Code action')
map('<leader>d', vim.diagnostic.open_float, 'Show diagnostics')
map('[d', function() vim.diagnostic.jump({ count = -1 }) end, 'Prev diagnostic')
map(']d', function() vim.diagnostic.jump({ count = 1 }) end, 'Next diagnostic')
map('<leader>d', vim.diagnostic.open_float, 'Show diagnostics')
-- vim.diagnostic.jump() was added in nvim 0.10
if vim.fn.has('nvim-0.10') == 1 then
map('[d', function() vim.diagnostic.jump({ count = -1 }) end, 'Prev diagnostic')
map(']d', function() vim.diagnostic.jump({ count = 1 }) end, 'Next diagnostic')
else
map('[d', vim.diagnostic.goto_prev, 'Prev diagnostic')
map(']d', vim.diagnostic.goto_next, 'Next diagnostic')
end

-- LSP word highlight — replaces vim-illuminate (semantic, not regex)
-- Use a buffer-keyed augroup so multiple servers attaching to the same
Expand All @@ -131,41 +139,52 @@ require('lazy').setup({
end,
})

vim.lsp.config('pyright', {
capabilities = capabilities,
settings = {
python = {
analysis = {
useLibraryCodeForTypes = true, -- infer types from lib source when stubs absent
-- Server configs defined once; registration method differs by nvim version.
-- nvim 0.11+: vim.lsp.config/enable (new built-in API, no lspconfig on_attach)
-- nvim 0.9–0.10: lspconfig.server.setup() (classic API)
local servers = {
pyright = {
capabilities = capabilities,
settings = { python = { analysis = { useLibraryCodeForTypes = true } } },
},
clangd = { capabilities = capabilities },
bashls = { capabilities = capabilities },
lua_ls = {
capabilities = capabilities,
settings = {
Lua = {
runtime = { version = 'LuaJIT' },
workspace = { library = { vim.env.VIMRUNTIME }, checkThirdParty = false },
diagnostics = { globals = { 'vim' } },
},
},
},
})
for _, server in ipairs({ 'clangd', 'bashls' }) do
vim.lsp.config(server, { capabilities = capabilities })
}

if vim.fn.has('nvim-0.11') == 1 then
require('mason-lspconfig').setup({
ensure_installed = vim.tbl_keys(servers),
automatic_enable = false, -- we call vim.lsp.enable() below
})
for name, cfg in pairs(servers) do vim.lsp.config(name, cfg) end
vim.lsp.enable(vim.tbl_keys(servers))
else
require('mason-lspconfig').setup({ ensure_installed = vim.tbl_keys(servers) })
local lspconfig = require('lspconfig')
for name, cfg in pairs(servers) do lspconfig[name].setup(cfg) end
end
vim.lsp.config('lua_ls', {
capabilities = capabilities,
settings = {
Lua = {
runtime = { version = 'LuaJIT' },
workspace = { library = { vim.env.VIMRUNTIME }, checkThirdParty = false },
diagnostics = { globals = { 'vim' } },
},
},
})
vim.lsp.enable({ 'pyright', 'clangd', 'bashls', 'lua_ls' })

vim.diagnostic.config({
severity_sort = true,
float = { border = 'rounded', source = true }, -- show server name per diagnostic
float = { border = 'rounded', source = true },
})
end,
},

-- ── Completion ───────────────────────────────────────────────────────────
{
'saghen/blink.cmp',
cond = vim.fn.has('nvim-0.10') == 1, -- uses vim.snippet built-in (nvim 0.10+)
version = '*', -- use release tags (pre-built Rust binary)
dependencies = { 'rafamadriz/friendly-snippets' },
config = function()
Expand Down Expand Up @@ -201,18 +220,27 @@ require('lazy').setup({
-- ── Treesitter ───────────────────────────────────────────────────────────
{
'nvim-treesitter/nvim-treesitter',
cond = vim.fn.has('nvim-0.10') == 1, -- uses vim.fs.joinpath (nvim 0.10+)
lazy = false,
build = ':TSUpdate',
dependencies = {
'nvim-treesitter/nvim-treesitter-textobjects',
{
'nvim-treesitter/nvim-treesitter-context',
cond = vim.fn.has('nvim-0.10') == 1, -- uses LspRequest event (nvim 0.10+)
config = function()
require('treesitter-context').setup({ max_lines = 3 })
end,
},
},
config = function()
-- Ensure parsers are present on fresh installs (async, no-op if already installed).
require('nvim-treesitter').install({
'bash', 'c', 'cpp', 'css', 'go', 'html', 'javascript',
'json', 'lua', 'markdown', 'python', 'rust', 'toml',
'typescript', 'vim', 'yaml',
})

-- Highlighting: built-in vim.treesitter, enabled per filetype
vim.api.nvim_create_autocmd('FileType', {
group = vim.api.nvim_create_augroup('UserTreesitter', { clear = true }),
Expand Down Expand Up @@ -439,6 +467,7 @@ require('lazy').setup({
{ 'andymass/vim-matchup', event = 'BufReadPost' },
{
'echasnovski/mini.ai',
cond = vim.fn.has('nvim-0.10') == 1,
event = 'VeryLazy',
config = function()
require('mini.ai').setup({
Expand Down Expand Up @@ -471,6 +500,7 @@ require('lazy').setup({
},
{
'echasnovski/mini.bracketed',
cond = vim.fn.has('nvim-0.10') == 1,
event = 'VeryLazy',
config = function()
require('mini.bracketed').setup({
Expand Down
57 changes: 57 additions & 0 deletions scripts/install-neovim-src.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Build and install the latest stable neovim from source.
# Required on systems where glibc < 2.32 (e.g. Ubuntu 20.04) prevents
# running the prebuilt GitHub release binaries.
#
# Usage: bash scripts/install-neovim-src.sh
# NEOVIM_TAG=v0.12.1 bash scripts/install-neovim-src.sh # pin version
# NEOVIM_PREFIX=~/.local bash scripts/install-neovim-src.sh # nosudo install
set -euo pipefail

# ── Preflight ─────────────────────────────────────────────────────────────────
if ! sudo -n true 2>/dev/null && ! sudo -v 2>/dev/null; then
echo "ERROR: sudo is required to install build deps and copy files to /usr/local" >&2
exit 1
fi
echo "Note: this build downloads ~200 MB of dependencies and takes 5–15 minutes."

PREFIX="${NEOVIM_PREFIX:-/usr/local}"

# ── Build dependencies ────────────────────────────────────────────────────────
echo "── Installing build dependencies ──"
sudo apt-get install -y \
git ninja-build gettext cmake unzip curl build-essential

# ── Resolve tag ──────────────────────────────────────────────────────────────
if [ -z "${NEOVIM_TAG:-}" ]; then
NEOVIM_TAG=$(curl -sfL \
https://api.github.com/repos/neovim/neovim/releases/latest \
| grep -o '"tag_name": *"[^"]*"' \
| grep -o 'v[^"]*')
fi
[ -n "$NEOVIM_TAG" ] || { echo "ERROR: could not resolve latest neovim tag" >&2; exit 1; }
echo "── Building neovim $NEOVIM_TAG → $PREFIX ──"

# ── Clone ─────────────────────────────────────────────────────────────────────
TMP=$(mktemp -d)
# shellcheck disable=SC2064
trap "rm -rf '$TMP'" EXIT

git clone --branch "$NEOVIM_TAG" --depth 1 \
https://github.com/neovim/neovim.git "$TMP/neovim"

cd "$TMP/neovim"

# ── Build ─────────────────────────────────────────────────────────────────────
cmake -B build \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$PREFIX"

cmake --build build -j"$(nproc)"

# ── Install ───────────────────────────────────────────────────────────────────
sudo cmake --install build

VER=$("$PREFIX/bin/nvim" --version 2>/dev/null | head -1) || VER="(unknown)"
echo "✓ $VER installed → $PREFIX"
Loading