Neovim for DevOps Engineers: From SSH Terminals to Full Development

Neovim for DevOps Engineers: From SSH Terminals to Full Development

A comprehensive guide to why Neovim is the superior editor for DevOps work. Master remote development, infrastructure-as-code workflows, minimal dependencies, and why traditional IDEs fail in SSH environments. Learn the mental model of why Neovim beats VS Code and JetBrains when you're managing infrastructure.

By Omar Flores

Neovim for DevOps Engineers: From SSH Terminals to Full Development

Why Modal Editing and Minimal Dependencies Beat GUI-Heavy IDEs in Infrastructure Work


🎯 Introduction: The Problem DevOps Engineers Face

Let me start with a painful reality: Most DevOps engineers are using the wrong editor.

Not because they’re making bad choices. But because they’re forced to work in environments where traditional editors fail catastrophically.

The Scenario

You’re managing infrastructure across multiple environments. You SSH into:

  • A production server at 3 AM because something broke
  • A Kubernetes control plane to debug a networking issue
  • A developer’s machine to help troubleshoot their setup
  • A CI/CD runner that has 500MB of disk space
  • A bastion host with only vim installed

In these moments, VS Code? Doesn’t exist. JetBrains IDE? Can’t run. You need an editor that:

βœ… Runs on minimal resources
βœ… Works over SSH instantly
βœ… Requires no GUI infrastructure
βœ… Handles large infrastructure-as-code files efficiently
βœ… Integrates with Unix tools you already use
βœ… Doesn't require X11 forwarding or complex networking
βœ… Starts in milliseconds
βœ… Works perfectly over high-latency SSH connections

This is not a hypothetical problem. This is the daily reality of DevOps work.

The Traditional Problem

DevOps engineers reach for two options:

Option 1: Use vi/vim
- Works everywhere
- But learning curve is steep
- Configuration is obtuse
- No modern features (LSP, fuzzy search, etc.)
- Time investment for minimal gain

Option 2: Use VS Code with Remote SSH
- Modern experience
- Good when working on ONE machine
- But:
  - Requires Node.js installed on remote
  - Requires 200MB+ disk space for server
  - Connection drops = lost editing session
  - Complex setup for bastion hosts / jump servers
  - Can't run on 500MB constrained environments
  - Polling for file changes is expensive
  - Security team blocks it (forwarded ports)

Neither option is optimal for infrastructure work.

What This Guide Is About

This is not β€œLearn Neovim in 100 easy steps.”

This is: Why Neovim is architecturally superior for DevOps work, and how to use it effectively in real infrastructure scenarios.

We will cover:

βœ… Why Neovim exists - The philosophy that made it better
βœ… Remote development model - How it actually works over SSH
βœ… Infrastructure-as-code workflows - Terraform, Ansible, scripts
βœ… Minimal dependencies - Why this matters
βœ… SSH scenarios - Bastion hosts, jump servers, production boxes
βœ… Configuration management - Making Neovim your own
βœ… DevOps-specific setup - LSP, linting, formatting for IaC
βœ… Productivity patterns - The workflows that actually matter

The perspective: How a senior DevOps engineer thinks about editor choices.


πŸ—οΈ Part 1: Why Neovim Exists (The Architectural Difference)

Before diving into how to use Neovim, understand why it was created and how that changes everything.

The Problem with Vim

Vim is incredible. It runs everywhere. It’s fast. It’s been refined for 30 years.

But Vim has fundamental architectural limitations:

Vim Architecture (simplified):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Core Editor Engine     β”‚
β”‚   (written in C, 1991)      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   VimL scripting language   β”‚
β”‚   (custom language, hard)   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   Plugin system (VimL)      β”‚
β”‚   (limited, slow, isolated) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Problems:
- Plugins are isolated processes (slow)
- VimL is a custom language (steep learning curve)
- Can't easily integrate modern tools (LSP, etc.)
- Configuration requires learning VimL
- Community fragmented between Vim and plugin authors

Neovim was created to fix these problems:

Neovim Architecture (Modern)

Neovim Architecture (redesigned):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Core Editor Engine             β”‚
β”‚   (extracted from Vim, refactored)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   API Layer (msgpack RPC protocol)  β”‚
β”‚   (real network protocol)           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   Plugin Ecosystem                  β”‚
β”‚   - Lua scripting (fast, modern)    β”‚
β”‚   - External processes (real async) β”‚
β”‚   - Language servers (LSP native)   β”‚
β”‚   - Terminal integration            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Advantages:
- Plugins talk to Neovim via RPC (real async)
- Lua is fast and modern (not VimL)
- Native LSP support (language servers)
- External processes aren't blocked
- Configuration is code, not commands

This architectural difference means:

Vim: "How do I make my editor do this?"
Answer: "Learn VimL and hope someone wrote a plugin"

Neovim: "How do I make my editor do this?"
Answer: "Write Lua code or use an existing plugin"

Why This Matters for DevOps

DevOps engineers work with:

  • Multiple languages: Terraform, Python, Go, Bash, YAML, JSON
  • LSP servers: pyright, gopls, terraform-ls, etc.
  • Formatting tools: black, gofmt, prettier
  • Linting: pylint, golangci-lint, terraform validate
  • Integration tools: git, grep, find, make

Vim makes integrating these difficult. Neovim makes it native.

In Vim:
- Install language plugins
- Hope they work together
- Configuration is command-based
- Async requires vim-async-plugin

In Neovim:
- LSP integrates natively (language-agnostic)
- Async is built-in
- Configuration is Lua code (composable)
- Tools integrate cleanly

For DevOps work, where you’re constantly switching between tools and languages, Neovim’s architecture is superior.


🌐 Part 2: Remote Development Model - How SSH Works with Neovim

The key to understanding Neovim for DevOps is understanding its remote development model.

The Three SSH Scenarios

Scenario 1: Direct SSH Connection

You SSH into a server and run Neovim directly:

ssh user@server.com
neovim /path/to/file

How it works:

Your Computer             Server
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   SSH       β”‚          β”‚  Neovim     β”‚
β”‚   Terminal  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€  Process    β”‚
β”‚             β”‚          β”‚  (remote)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

- All editing happens on server
- Only terminal data transmitted (tiny)
- No files transferred
- Works over high-latency connections
- Works over low-bandwidth connections

Advantages:

βœ… No setup needed on server
βœ… Minimal disk space (Neovim = 10MB)
βœ… Minimal RAM (Neovim = 50MB)
βœ… Works over unreliable connections
βœ… No security concerns (no forwarding)
βœ… File changes instant (no polling)
βœ… Kernel tools available on server

Scenario 2: SSH with Neovim Remote (netrw)

You run Neovim locally but edit files on remote server via SSH:

nvim scp://user@server.com/path/to/file

How it works:

Your Computer (GUI/Terminal)     Server
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Neovim Process        β”‚      β”‚   SSH    β”‚
β”‚   - Editing locally     β”œβ”€β”€β”€β”€β”€β”€β”€ Protocol β”‚
β”‚   - Files synced via    β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚     SSH/SCP             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

- Editing happens locally
- Files transferred on save
- Your configuration used
- Server just stores files

Advantages:

βœ… Use your local Neovim configuration
βœ… Local plugins available
βœ… Faster editing (no network latency)
βœ… Works with any remote file system

Disadvantages:

❌ Requires Neovim on local machine
❌ Requires SSH keys set up
❌ File syncing can lag
❌ Not good for collaborative work

Scenario 3: SSH with Local SSH Control

You SSH to a machine, but Neovim connects BACK to your local machine:

# On remote server:
ssh -R 22:localhost:22 user@local-machine
# Inside that SSH:
nvim scp://local-user@localhost/path/to/file

Advanced scenario (rarely needed).

The Key Insight: Terminal-Based Editing Scales

VS Code Remote SSH Model:
- VS Code runs locally
- Node.js server runs on remote
- WebSocket connection
- File polling/watching
- GUI elements transmitted
- Complex setup required
- Doesn't work in constrained environments

Neovim SSH Model:
- Neovim runs on remote (if needed)
- Or files accessed via SSH protocol
- Only terminal/keyboard data transmitted
- Minimal server resources
- Works in constrained environments
- Scale to hundreds of remote servers

This is why Neovim scales for DevOps. You can manage 50 servers, SSH to any one of them, edit infrastructure code instantly, with zero setup.

Real-World SSH Scenarios

Scenario A: Production Hotfix at 3 AM

Your machine (home, spotty WiFi)
    ↓ SSH
Production Bastion Host
    ↓ SSH
Production Server (down due to config error)

Traditional approach:
- VS Code can't connect through bastion (networking complexity)
- Have to tunnel, port forward, configure jump servers
- Setup takes 20 minutes
- Meanwhile service is down

Neovim approach:
ssh bastion-host
ssh production-server
nvim /etc/myapp/config.yaml
# Fix immediately
# Done in 1 minute

Scenario B: Terraform Deployment to Multiple Regions

You have:
- dev environment (AWS)
- staging environment (AWS)
- prod-us (AWS)
- prod-eu (AWS)
- prod-asia (AWS)

Traditional approach:
- Set up VS Code Remote on each environment
- Each setup takes 30 minutes
- 5 environments = 2.5 hours setup
- Switching between environments = context switch

Neovim approach:
# Alias in your ~/.ssh/config:
Host dev-terraform
    HostName dev-bastion.internal
    User terraform-deploy

# Then:
ssh dev-terraform
nvim terraform/main.tf
# Done, switch to another environment instantly

Scenario C: Debugging Ansible on Deployment Host

Your machine β†’ SSH β†’ Deployment host (has Ansible playbooks)

Playbook fails. Need to edit it.

Traditional:
- Option 1: SCP file locally, edit, SCP back (slow)
- Option 2: VS Code Remote (overkill, needs setup)
- Option 3: nano/vi (painful)

Neovim:
ssh deployment-host
nvim playbooks/deploy.yml
# Edit directly, test immediately

πŸ”§ Part 3: Infrastructure-as-Code Workflows

DevOps engineers primarily work with IaC. Let’s examine how Neovim excels here.

The Tools DevOps Uses

Terraform:
- *.tf files
- terraform-ls (language server)
- terraform fmt, terraform validate

Ansible:
- *.yml, *.yaml
- ansible-lint
- ansible-playbook (testing)

Kubernetes:
- *.yaml
- YAML files (no special tools)
- kubectl apply --dry-run (testing)

Python (automation scripts):
- *.py files
- pyright, pylint (linting)
- pytest (testing)

Bash/Shell (maintenance scripts):
- *.sh files
- shellcheck (linting)
- bash -n (validation)

Go (infrastructure tools):
- *.go files
- gopls (language server)
- go fmt, go vet

Traditional IDEs struggle here because:

❌ You're constantly switching languages
❌ No single IDE handles all of them well
❌ JetBrains has separate IDEs for Python, Go, etc.
❌ VS Code needs 20+ extensions
❌ Configuration becomes fragmented

Neovim handles this elegantly:

Neovim as Universal IaC Editor

Single configuration (init.lua):

1. Install LSP servers for each language
   - terraform-ls
   - pyright
   - gopls
   - yamllint (for YAML)

2. Configure formatters
   - terraform fmt
   - black (Python)
   - gofmt (Go)
   - prettier (JSON/YAML)

3. Configure linters
   - terraform validate
   - pylint
   - golangci-lint
   - ansible-lint
   - shellcheck

4. Add file type detection
   - *.tf β†’ Terraform
   - *.py β†’ Python
   - *.go β†’ Go
   - *.yaml β†’ YAML

5. All languages use same keybindings
   - gd β†’ Go to definition
   - gr β†’ Find references
   - K β†’ Show documentation
   - Space+f β†’ Format

One editor. One configuration. All languages.

Real-World IaC Workflow Example

# terraform/main.tf - you're editing this

resource "aws_ec2_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"  # ← cursor here

  tags = {
    Name = "web-server"
  }
}

With Neovim + terraform-ls:

Press: K
Shows: AWS documentation for instance_type parameter
Shows: Valid values, examples, defaults

Press: gd
Jumps to: Where this ami is defined

Press: gr
Shows: All places using this ami ID

Press: Space+f
Runs: terraform fmt on the file
Reformats: Entire file according to Terraform standards

Press: ,t (custom keybinding)
Runs: terraform validate
Shows: Errors/warnings inline

All happening instantly, with no setup beyond initial Neovim config.

Infrastructure Testing in Neovim

# Edit your Ansible playbook
nvim deploy.yml

# Save the file (Neovim auto-formats with prettier/yamllint)

# Run the playbook in a split window
:terminal ansible-playbook deploy.yml --check

# Errors show up immediately

# Fix in one pane, test in another pane

# Switch panes (Neovim built-in splits)
Ctrl+w β†’ (move to terminal pane)

This workflow is:

βœ… Fast (no file syncing)
βœ… Direct (see results instantly)
βœ… Integrated (all in one editor)
βœ… Flexible (your keyboard shortcuts)

πŸ“¦ Part 4: Minimal Dependencies (Why This Matters)

A core principle of DevOps is: minimize dependencies, maximize reliability.

This principle applies to your editor too.

Dependency Analysis

VS Code Remote SSH:

Requirements on remote server:
- Node.js β‰₯ 14
- 200MB+ disk space
- ssh-server
- curl or wget (for installation)

Disk footprint: 200-400MB
RAM usage: 300-500MB minimum
Startup time: 5-10 seconds
Complexity: Moderate (debugging remote connection issues is painful)

JetBrains IDEs:

Requirements on remote server:
- Java JVM β‰₯ 11
- 500MB+ disk space
- ssh-server

Disk footprint: 500MB-1GB
RAM usage: 800MB-1.5GB minimum
Startup time: 10-20 seconds
Complexity: High (JVM configuration, memory tuning)

Neovim:

Requirements on remote server:
- Linux/Unix kernel
- That's it

Disk footprint: 10-20MB (including plugins)
RAM usage: 50-100MB typical
Startup time: <100ms
Complexity: Low (pure Unix tooling)

Why Minimal Dependencies Matter

Scenario 1: Constrained Container Environment

Your CI/CD container has 500MB disk limit.

VS Code Remote: ❌ Won't fit
JetBrains: ❌ Won't fit
Neovim: βœ… Easily fits

Scenario 2: Running on Embedded System

You're managing OpenWrt router firmware configuration.

Hardware: 128MB RAM, 8MB flash storage

VS Code: ❌ Impossible
JetBrains: ❌ Impossible
Neovim: βœ… Runs fine

Scenario 3: Air-Gapped Environment

You have a machine that can't download packages from internet.
You need to provide pre-built editor.

VS Code Remote: ❌ Hundreds of NPM dependencies, complex binary
JetBrains: ❌ JVM complexity, many dependencies
Neovim: βœ… Single 15MB binary, zero runtime dependencies

Scenario 4: Serverless/Lambda Function Debugging

You have AWS Lambda@Edge function that needs editing.

Can't use: VS Code (too heavy), JetBrains (too heavy)
Can use: vim (painful), nano (very painful), Neovim (excellent)

Dependency Chain Matters

VS Code dependencies:
node.js β†’ v8 β†’ OpenSSL β†’ glibc β†’ kernel

That's 4 dependencies between you and the kernel.
Each one is a potential point of failure.
Each one needs security updates.

Neovim dependencies:
libc β†’ kernel

That's 1 dependency.
Neovim itself is the trusted layer.
Single binary, simple to audit.

For infrastructure work, where reliability is paramount, minimal dependencies is a feature, not a limitation.

Self-Contained Deployments

DevOps principle: carry your tools with you.

Neovim approach:
1. Build Neovim from source in your CI system
2. Static binary (no runtime dependencies)
3. SCP to any server (no installation needed)
4. Works anywhere, instantly

VS Code approach:
1. Install Node.js on server
2. Download 200MB VSCode
3. Configure everything
4. Deal with platform differences
5. Troubleshoot when something breaks

πŸ” Part 5: Security and Reliability Implications

Using minimal-dependency tools has security consequences.

Attack Surface

VS Code Remote SSH:
- Node.js process (potential vulnerability)
- WebSocket server (potential vulnerability)
- File watching system (potential vulnerability)
- Authentication in VS Code (potential vulnerability)

Risk: More code = more potential exploits

Neovim:
- Single binary (well-audited, 30 years history)
- SSH protocol (network layer, not application layer)
- No background services

Risk: Minimal, focused

Supply Chain Security

VS Code gets updates weekly.
Each update is a risk.
"Security update" means: there was a vulnerability.

Neovim gets security updates occasionally.
Simpler codebase = fewer vulnerabilities.
Single responsibility = easier to audit.

Reliability in Production

Production emergency 3 AM:
You need to fix a configuration file IMMEDIATELY.

With VS Code Remote:
- Is Node.js running?
- Is the remote connection stable?
- Is disk full preventing server startup?
- Did WebSocket timeout?
- Need to restart server, reconnect, etc.

With Neovim:
- SSH connection exists? Edit directly.
- No setup needed.
- No processes to manage.
- Works in states where VS Code would fail.

βš™οΈ Part 6: DevOps-Specific Neovim Configuration

Now that you understand WHY Neovim, let’s set it up FOR DevOps WORK.

Essential Plugins for DevOps

-- ~/.config/nvim/init.lua

-- Plugin Manager (using packer.nvim)
local ensure_packer = function()
  local fn = vim.fn
  local install_path = fn.stdpath('data')..'/site/pack/packer/start/packer.nvim'
  if fn.empty(fn.glob(install_path)) > 0 then
    fn.system({'git', 'clone', '--depth', '1', 'https://github.com/wbthomason/packer.nvim', install_path})
    return true
  end
  return false
end

local packer_bootstrap = ensure_packer()

require('packer').startup(function(use)
  -- LSP Configuration
  use 'neovim/nvim-lspconfig'
  use 'williamboman/mason.nvim'
  use 'williamboman/mason-lspconfig.nvim'

  -- Completion
  use 'hrsh7th/nvim-cmp'
  use 'hrsh7th/cmp-nvim-lsp'
  use 'hrsh7th/cmp-buffer'
  use 'hrsh7th/cmp-path'

  -- Snippets
  use 'L3MON4D3/LuaSnip'
  use 'saadparwaiz1/cmp_luasnip'

  -- Telescope (fuzzy finder)
  use 'nvim-telescope/telescope.nvim'
  use 'nvim-lua/plenary.nvim'

  -- Treesitter (syntax highlighting)
  use 'nvim-treesitter/nvim-treesitter'

  -- Git Integration
  use 'tpope/vim-fugitive'
  use 'lewis6991/gitsigns.nvim'

  -- File Explorer
  use 'kyazdani42/nvim-tree.lua'

  -- Status Line
  use 'nvim-lualine/lualine.nvim'

  -- Theme
  use 'folke/tokyonight.nvim'

  if packer_bootstrap then
    require('packer').sync()
  end
end)

LSP Setup for Infrastructure Languages

-- ~/.config/nvim/after/plugin/lspconfig.lua

local lspconfig = require('lspconfig')
local mason_lspconfig = require('mason-lspconfig')

-- Automatically install language servers for these languages
mason_lspconfig.setup({
  ensure_installed = {
    'terraformls',  -- Terraform
    'pyright',      -- Python
    'gopls',        -- Go
    'yamlls',       -- YAML
    'bashls',       -- Bash
    'jsonls',       -- JSON
  },
  automatic_installation = true,
})

-- Configure each language server
local lsp_config = {
  on_attach = function(client, bufnr)
    -- Enable completion
    vim.bo.omnifunc = 'v:lua.vim.lsp.omnifunc'

    -- Keybindings
    local opts = { noremap = true, silent = true, buffer = bufnr }
    vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)        -- Go to definition
    vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)         -- Find references
    vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)               -- Show documentation
    vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)    -- Rename
    vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, opts) -- Code actions
    vim.keymap.set('n', '<leader>f', vim.lsp.buf.format, opts)     -- Format
  end,
  capabilities = require('cmp_nvim_lsp').default_capabilities(),
}

-- Terraform
lspconfig.terraformls.setup(lsp_config)

-- Python
lspconfig.pyright.setup(lsp_config)

-- Go
lspconfig.gopls.setup(lsp_config)

-- YAML (for Ansible, Kubernetes)
lspconfig.yamlls.setup(lsp_config)

-- Bash
lspconfig.bashls.setup(lsp_config)

-- JSON
lspconfig.jsonls.setup(lsp_config)

-- Formatters
vim.api.nvim_command('autocmd BufWritePre *.tf silent! exec "!terraform fmt %"')
vim.api.nvim_command('autocmd BufWritePre *.py silent! exec "!black %"')
vim.api.nvim_command('autocmd BufWritePre *.go silent! exec "!gofmt -w %"')
vim.api.nvim_command('autocmd BufWritePre *.yml,*.yaml silent! exec "!prettier --write %"')

DevOps-Specific Keybindings

-- ~/.config/nvim/init.lua (keybindings section)

-- Leader key
vim.g.mapleader = ' '

-- File operations
vim.keymap.set('n', '<leader>w', ':w<CR>', { noremap = true, silent = true })
vim.keymap.set('n', '<leader>q', ':q<CR>', { noremap = true, silent = true })
vim.keymap.set('n', '<leader>x', ':x<CR>', { noremap = true, silent = true })

-- Terraform specific
vim.keymap.set('n', '<leader>tv', ':!terraform validate<CR>', { noremap = true, silent = true })
vim.keymap.set('n', '<leader>tp', ':!terraform plan<CR>', { noremap = true, silent = true })
vim.keymap.set('n', '<leader>ta', ':!terraform apply<CR>', { noremap = true, silent = true })

-- Ansible specific
vim.keymap.set('n', '<leader>al', ':!ansible-lint %<CR>', { noremap = true, silent = true })
vim.keymap.set('n', '<leader>ac', ':!ansible-playbook % --check<CR>', { noremap = true, silent = true })

-- Terminal
vim.keymap.set('n', '<leader>t', ':terminal<CR>', { noremap = true, silent = true })
vim.keymap.set('t', '<Esc>', '<C-\\><C-n>', { noremap = true, silent = true })

-- Window navigation
vim.keymap.set('n', '<C-h>', '<C-w>h', { noremap = true, silent = true })
vim.keymap.set('n', '<C-j>', '<C-w>j', { noremap = true, silent = true })
vim.keymap.set('n', '<C-k>', '<C-w>k', { noremap = true, silent = true })
vim.keymap.set('n', '<C-l>', '<C-w>l', { noremap = true, silent = true })

-- Fuzzy finder
vim.keymap.set('n', '<leader>ff', ':Telescope find_files<CR>', { noremap = true, silent = true })
vim.keymap.set('n', '<leader>fg', ':Telescope live_grep<CR>', { noremap = true, silent = true })
vim.keymap.set('n', '<leader>fb', ':Telescope buffers<CR>', { noremap = true, silent = true })

-- Git
vim.keymap.set('n', '<leader>gs', ':Git status<CR>', { noremap = true, silent = true })
vim.keymap.set('n', '<leader>gc', ':Git commit<CR>', { noremap = true, silent = true })
vim.keymap.set('n', '<leader>gp', ':Git push<CR>', { noremap = true, silent = true })

SSH Config for Easy Access

# ~/.ssh/config

# Production infrastructure
Host prod-bastion
    HostName prod-bastion.company.com
    User devops
    IdentityFile ~/.ssh/id_rsa
    ControlMaster auto
    ControlPath ~/.ssh/control-%h-%p-%r
    ControlPersist 600

Host prod-*
    ProxyCommand ssh prod-bastion -W %h:%p
    User devops
    IdentityFile ~/.ssh/id_rsa

# Staging infrastructure
Host staging-bastion
    HostName staging-bastion.company.com
    User devops
    IdentityFile ~/.ssh/id_rsa

Host staging-*
    ProxyCommand ssh staging-bastion -W %h:%p
    User devops
    IdentityFile ~/.ssh/id_rsa

# Development
Host dev-*
    HostName %h.company-dev.internal
    User devops
    IdentityFile ~/.ssh/id_rsa

Now you can:

# Jump to production and edit:
ssh prod-terraform-server
nvim terraform/main.tf

# Edit files locally from remote:
nvim scp://prod-terraform-server//home/devops/terraform/main.tf

# All configured, all keybindings ready

πŸ“‹ Part 7: Real-World DevOps Scenarios

Let’s walk through actual workflows.

Scenario 1: Emergency Production Hotfix

Time: 3 AM
Issue: API returning 500 errors
Root cause: Configuration file corrupt

The workflow:

# 1. SSH to production (through bastion)
ssh prod-api-server

# 2. Open config file in Neovim
nvim /etc/myapp/config.yaml

# 3. Neovim opens (instantly, <100ms)
# 4. Find the issue (syntax error in YAML)
# 5. Fix it (modal editing is fast once you know it)
# 6. Save with auto-formatting
# 7. Validate configuration
#    :terminal
#    ./validate-config.sh /etc/myapp/config.yaml

# 8. Restart service
#    !/etc/init.d/myapp restart

# 9. Monitor logs
#    :terminal tail -f /var/log/myapp/error.log

# 10. Confirm fix (2 minutes total)

Time breakdown:

  • Open Neovim: <1 second
  • Edit: 30 seconds
  • Validate: 10 seconds
  • Restart: 20 seconds
  • Verify: 30 seconds
  • Total: ~2 minutes

With VS Code Remote: 15+ minutes (setup connection, wait for initialization, etc.)

Scenario 2: Terraform Multi-Environment Deployment

Task: Deploy new infrastructure to 3 regions simultaneously

Environments:
- prod-us (terraform/aws-us/main.tf)
- prod-eu (terraform/aws-eu/main.tf)
- prod-asia (terraform/aws-asia/main.tf)

The workflow:

# Terminal 1: Start editing
ssh terraform-deploy-server
nvim terraform/

# Neovim opens file browser
# Ctrl+t opens split
# Open 3 files in tabs

# Tab 1: aws-us/main.tf
# Make changes to infrastructure definition
# Space+f β†’ Auto-format
# Space+tv β†’ Validate with terraform

# Tab 2: aws-eu/main.tf
# Make same changes

# Tab 3: aws-asia/main.tf
# Make same changes

# Split window to terminal
:terminal

# In terminal:
terraform plan -out=plan-us terraform/aws-us
terraform plan -out=plan-eu terraform/aws-eu
terraform plan -out=plan-asia terraform/aws-asia

# Review all plans simultaneously in split windows
# If happy:
terraform apply plan-us &
terraform apply plan-eu &
terraform apply plan-asia &

# Watch progress
tail -f /tmp/terraform-apply.log

Advantages:

  • All 3 regions edited in one session
  • Local keybindings work consistently
  • Can see all code simultaneously
  • Terminal access for testing/validation
  • No file syncing issues

Scenario 3: Debugging Ansible Playbook

Issue: Ansible playbook fails on 10 hosts
Need to: Inspect playbook, edit, test incrementally

The workflow:

ssh ansible-control-server
nvim playbooks/deploy.yml

# In editor:
# - View playbook (syntax highlighting with Treesitter)
# - LSP shows YAML errors before running
# - Cursor over variable name
#   - gr β†’ Find all references to this variable
#   - gd β†’ Find where defined
# - Edit a task

# Split terminal:
:split
:terminal

# In terminal pane:
ansible-playbook playbooks/deploy.yml --check --limit test-host-01

# See output in real-time
# Go back to editor pane, fix error
# Re-run test

# Iterate until perfect

Advantages:

  • Edit and test in same window
  • No file transfers needed
  • See results immediately
  • All in one cohesive workflow

πŸŽ“ Part 8: The Learning Curve (Honest Assessment)

Neovim is powerful. It also has a learning curve.

Week 1: The Basics (4-6 hours)

Learn:
- hjkl navigation (move cursor without arrow keys)
- i/I β†’ insert mode
- ESC β†’ normal mode
- :w β†’ save
- :q β†’ quit
- dd β†’ delete line
- yy β†’ copy line
- p β†’ paste

Practice: 30 minutes daily
By end: Can edit files slowly but surely

Week 2: Modal Editing (4-6 hours)

Learn:
- w/b β†’ jump by word
- {/} β†’ jump by paragraph
- /{pattern} β†’ search
- n β†’ next result
- * β†’ search word under cursor
- / $ β†’ end of line
- 0 β†’ beginning of line
- G β†’ end of file
- gg β†’ beginning of file

Practice: 30 minutes daily
By end: Navigation is fast

Week 3: Editing Efficiently (4-6 hours)

Learn:
- ci" β†’ change inside quotes
- da( β†’ delete around parentheses
- v β†’ visual mode
- V β†’ visual line mode
- Ctrl+v β†’ visual block mode
- . β†’ repeat last command

Practice: 30 minutes daily
By end: Editing is faster than mouse clicking

Month 2: Personalization (varies)

Learn:
- Map custom keybindings
- Install LSP for your languages
- Configure formatters
- Build your configuration

By end: Neovim works exactly for YOUR workflow

The Honest Truth

If you only need to:
- Edit files occasionally
- Do basic text editing
- Not invest time learning

Then: VS Code is better

If you are:
- Working with infrastructure daily
- Editing multiple languages
- In SSH environments regularly
- Want fast, minimal workflow
- Willing to invest 20 hours learning

Then: Neovim is worth it (and you'll use it for 20 years)

The payoff:

Month 1: Slower than VS Code (learning)
Month 2: Similar speed to VS Code
Month 3+: Faster than VS Code

Total time investment: 20 hours
Time saved per year (DevOps role): 40+ hours
ROI: Incredibly positive

πŸ”„ Part 9: Integration with DevOps Workflows

Making Neovim part of your actual infrastructure workflow.

Git Workflow Integration

-- In your Neovim config, add Git integration

-- Fugitive (Git commands)
vim.keymap.set('n', '<leader>gs', ':Git status<CR>')
vim.keymap.set('n', '<leader>gd', ':Git diff<CR>')
vim.keymap.set('n', '<leader>ga', ':Git add %<CR>')
vim.keymap.set('n', '<leader>gc', ':Git commit<CR>')
vim.keymap.set('n', '<leader>gp', ':Git push<CR>')

Workflow:

# Edit infrastructure code
nvim terraform/main.tf

# Check git status
<leader>gs

# Add changes
<leader>ga

# Commit with message
<leader>gc

# Push to remote
<leader>gp

# All without leaving Neovim

CI/CD Integration

# Your CI/CD config (.github/workflows/deploy.yml)

- name: Validate Infrastructure
  run: |
    terraform fmt -check terraform/
    terraform validate terraform/

- name: Lint Ansible
  run: ansible-lint playbooks/

- name: Lint Shell Scripts
  run: shellcheck scripts/*.sh

Local pre-commit hook (before pushing):

#!/bin/bash
# .git/hooks/pre-commit

echo "Running pre-commit checks..."

# Format Terraform
terraform fmt -recursive terraform/

# Validate Terraform
terraform validate terraform/ || exit 1

# Lint Ansible
ansible-lint playbooks/ || exit 1

# Lint shell scripts
shellcheck scripts/*.sh || exit 1

echo "All checks passed!"

Now when you edit in Neovim and save:

1. Auto-format happens (configured in init.lua)
2. Linting happens
3. Visual feedback in editor
4. Fix issues immediately
5. When you git push, pre-commit hook runs
6. CI/CD validates again

Configuration Management Sync

# ~/.local/bin/sync-nvim-config

#!/bin/bash
# Sync Neovim config to all servers

SERVERS=(
  "prod-bastion"
  "staging-bastion"
  "dev-bastion"
)

for server in "${SERVERS[@]}"; do
  echo "Syncing Neovim config to $server..."
  rsync -az ~/.config/nvim/ $server:~/.config/nvim/
done

echo "Config synced to all servers"

Now your Neovim configuration is consistent across all servers you access:

# On your local machine
./sync-nvim-config

# Now when you SSH to any server:
ssh prod-bastion

# Neovim has identical configuration everywhere
# Same keybindings
# Same plugins
# Same LSPs
# Consistent experience

πŸ“Š Part 10: Comparison Matrix (Neovim vs Alternatives)

Let’s be honest about tradeoffs:

                    Neovim      VS Code Remote   JetBrains
────────────────────────────────────────────────────────────
SSH Performance     ⭐⭐⭐⭐⭐    ⭐⭐              ⭐
Dependencies        ⭐⭐⭐⭐⭐    ⭐⭐              ⭐
Resource Usage      ⭐⭐⭐⭐⭐    ⭐⭐              ⭐
Learning Curve      ⭐⭐        ⭐⭐⭐⭐⭐          ⭐⭐⭐
IDE Features        ⭐⭐⭐      ⭐⭐⭐⭐⭐          ⭐⭐⭐⭐⭐
Multi-Language      ⭐⭐⭐      ⭐⭐⭐⭐            ⭐⭐⭐
Customization       ⭐⭐⭐⭐⭐    ⭐⭐⭐            ⭐
Built-in GUI        ⭐          ⭐⭐⭐⭐⭐          ⭐⭐⭐⭐⭐
────────────────────────────────────────────────────────────

Best for:
- Neovim: Infrastructure work, remote SSH, minimal resources
- VS Code: Modern development, GUI features, learning curve
- JetBrains: Language-specific advanced features, enterprise

When to Choose Each

Choose Neovim if:

βœ… Working primarily via SSH
βœ… Editing infrastructure code
βœ… Multiple programming languages
βœ… Resource-constrained environments
βœ… Value keyboard efficiency
βœ… Want to invest in learning
βœ… Prefer Unix philosophy

Choose VS Code if:

βœ… Learning to code
βœ… Prefer GUI over keyboard
βœ… Want "it works out of box"
βœ… Mobile development primarily
βœ… Team uses VS Code extensions
βœ… Don't SSH frequently

Choose JetBrains if:

βœ… Language-specific (Python, Go, Rust)
βœ… Enterprise debugging needed
βœ… Want maximum IDE features
βœ… Have budget for licenses
βœ… Don't work over SSH

⚑ Part 11: Advanced Customization (Beyond Basics)

Once you’re comfortable with Neovim fundamentals, customization transforms it into your perfect tool.

Custom Command Palette

-- ~/.config/nvim/lua/commands.lua

-- Command palette with telescope
require('telescope').load_extension('command_palette')

-- Or create custom commands for DevOps workflows

vim.api.nvim_create_user_command(
  'TfPlan',
  function()
    vim.cmd(':terminal terraform plan')
  end,
  { desc = 'Run terraform plan' }
)

vim.api.nvim_create_user_command(
  'AnsibleLint',
  function(opts)
    local file = vim.fn.expand('%')
    vim.cmd(':terminal ansible-lint ' .. file)
  end,
  { desc = 'Lint current Ansible file' }
)

vim.api.nvim_create_user_command(
  'KubectlApply',
  function()
    vim.cmd(':terminal kubectl apply -f % --dry-run=client')
  end,
  { desc = 'Dry-run kubectl apply' }
)

vim.api.nvim_create_user_command(
  'ValidateYaml',
  function()
    vim.cmd(':terminal python3 -m yaml < %')
  end,
  { desc = 'Validate YAML syntax' }
)

Usage:

:TfPlan              β†’ Run terraform plan
:AnsibleLint         β†’ Lint Ansible playbook
:KubectlApply        β†’ Dry-run Kubernetes manifest
:ValidateYaml        β†’ Validate YAML file

Auto-Commands for Workflow Automation

-- ~/.config/nvim/lua/autocommands.lua

local augroup = vim.api.nvim_create_augroup
local autocmd = vim.api.nvim_create_autocmd

-- Auto-format on save
augroup('FormatOnSave', { clear = true })
autocmd('BufWritePre', {
  group = 'FormatOnSave',
  pattern = { '*.tf', '*.py', '*.go', '*.yaml', '*.json' },
  callback = function()
    vim.lsp.buf.format({ async = false })
  end,
})

-- Auto-run linting on save
augroup('LintOnSave', { clear = true })
autocmd('BufWritePost', {
  group = 'LintOnSave',
  pattern = { '*.tf', '*.py', '*.sh' },
  callback = function()
    local file = vim.fn.expand('%')
    if file:match('%.tf$') then
      os.execute('terraform validate ' .. file .. ' &')
    elseif file:match('%.py$') then
      os.execute('python3 -m py_compile ' .. file .. ' &')
    elseif file:match('%.sh$') then
      os.execute('shellcheck ' .. file .. ' &')
    end
  end,
})

-- Show git diff in status line
augroup('GitStatus', { clear = true })
autocmd('BufEnter', {
  group = 'GitStatus',
  callback = function()
    local git_status = vim.fn.system('git status --porcelain ' .. vim.fn.expand('%'))
    if git_status ~= '' then
      vim.notify('File has uncommitted changes', vim.log.levels.INFO)
    end
  end,
})

-- Auto-reload file if changed externally
augroup('AutoReload', { clear = true })
autocmd('FocusGained', {
  group = 'AutoReload',
  command = 'checktime',
})

Custom Snippets for Infrastructure Code

-- ~/.config/nvim/lua/snippets.lua
-- Using LuaSnip

local ls = require('luasnip')
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node

-- Terraform snippets
ls.add_snippets('terraform', {
  -- AWS EC2 instance
  s('awsec2', {
    t('resource "aws_instance" "'),
    i(1, 'name'),
    t('" {'),
    t('\n  ami           = "'),
    i(2, 'ami-0c55b159'),
    t('"'),
    t('\n  instance_type = "'),
    i(3, 't2.micro'),
    t('"'),
    t('\n  '),
    t('\n  tags = {'),
    t('\n    Name = "'),
    i(4, 'my-instance'),
    t('"'),
    t('\n  }'),
    t('\n}'),
  }),

  -- Kubernetes provider
  s('k8sprovider', {
    t('terraform {'),
    t('\n  required_providers {'),
    t('\n    kubernetes = {'),
    t('\n      source  = "hashicorp/kubernetes"'),
    t('\n      version = "'),
    i(1, '2.16.0'),
    t('"'),
    t('\n    }'),
    t('\n  }'),
    t('\n}'),
    t('\n'),
    t('\nprovider "kubernetes" {'),
    t('\n  config_path = "'),
    i(2, '~/.kube/config'),
    t('"'),
    t('\n}'),
  }),
})

-- Ansible snippets
ls.add_snippets('yaml', {
  s('ansible-task', {
    t('- name: '),
    i(1, 'Task name'),
    t('\n  '),
    i(2, 'module'),
    t(':\n    '),
    i(3, 'key'),
    t(': '),
    i(4, 'value'),
  }),

  s('ansible-handler', {
    t('handlers:'),
    t('\n  - name: '),
    i(1, 'Handler name'),
    t('\n    '),
    i(2, 'module'),
    t(':\n      '),
    i(3, 'key'),
    t(': '),
    i(4, 'value'),
  }),
})

Status Line Customization

-- ~/.config/nvim/lua/statusline.lua
-- Using lualine.nvim

require('lualine').setup({
  options = {
    icons_enabled = true,
    theme = 'tokyonight',
    component_separators = { left = '|', right = '|' },
    section_separators = { left = '', right = '' },
    disabled_filetypes = {
      statusline = {},
      winbar = {},
    },
    ignore_focus = {},
    always_divide_middle = true,
    globalstatus = false,
    refresh = {
      statusline = 1000,
      tabline = 1000,
      winbar = 1000,
    },
  },
  sections = {
    lualine_a = { 'mode' },
    lualine_b = {
      { 'branch', icon = '' },
      { 'diff', symbols = { added = '+', modified = '~', removed = '-' } },
    },
    lualine_c = {
      { 'filename', path = 1 },
      function()
        local lsp = vim.lsp.get_active_clients()
        if next(lsp) == nil then
          return ''
        end
        return 'πŸ”— ' .. table.concat(vim.tbl_map(function(c) return c.name end, lsp), ', ')
      end,
    },
    lualine_x = {
      { 'encoding' },
      { 'fileformat' },
      { 'filetype' },
    },
    lualine_y = { 'progress' },
    lualine_z = { 'location' },
  },
})

Shows:

  • Current mode (Normal, Insert, Visual)
  • Git branch and diff status
  • Active LSP servers
  • File encoding, format, type
  • Progress and location

Telescope Configuration (Fuzzy Finder)

-- ~/.config/nvim/lua/telescope-config.lua

require('telescope').setup({
  defaults = {
    vimgrep_arguments = {
      'rg',
      '--color=never',
      '--no-heading',
      '--with-filename',
      '--line-number',
      '--column',
      '--smart-case',
    },
    prompt_prefix = 'πŸ” ',
    selection_caret = '➜ ',
    entry_prefix = '  ',
    initial_mode = 'insert',
    selection_strategy = 'reset',
    sorting_strategy = 'ascending',
    layout_strategy = 'horizontal',
    layout_config = {
      horizontal = {
        mirror = false,
        preview_width = 0.55,
        results_width = 0.8,
      },
      vertical = {
        mirror = false,
      },
      width = 0.87,
      height = 0.80,
      preview_cutoff = 120,
    },
  },
  pickers = {
    find_files = {
      find_command = { 'rg', '--files', '--hidden', '--glob', '!.git' },
    },
    live_grep = {
      theme = 'ivy',
    },
    buffers = {
      show_all_buffers = true,
      previewer = true,
    },
  },
  extensions = {
    fzf = {
      fuzzy = true,
      override_generic_sorter = true,
      override_file_sorter = true,
      case_mode = 'smart_case',
    },
  },
})

-- Keybindings for Telescope
vim.keymap.set('n', '<leader>ff', ':Telescope find_files<CR>')
vim.keymap.set('n', '<leader>fg', ':Telescope live_grep<CR>')
vim.keymap.set('n', '<leader>fb', ':Telescope buffers<CR>')
vim.keymap.set('n', '<leader>fh', ':Telescope help_tags<CR>')
vim.keymap.set('n', '<leader>fr', ':Telescope lsp_references<CR>')
vim.keymap.set('n', '<leader>fd', ':Telescope lsp_definitions<CR>')

🐹 Part 12: Golang in Neovim (Advanced Development Workflow)

Go is increasingly used in DevOps (Kubernetes, Terraform providers, infrastructure tools). Neovim becomes exceptional for Go development.

Why Go + Neovim is Perfect

Go philosophy:
- Simplicity (one way to do things)
- Fast compilation
- Built-in tooling (gofmt, go vet, etc.)
- Cross-platform compilation
- Static binaries

Neovim philosophy:
- Simplicity (modal editing, composability)
- Minimal dependencies
- Built-in tooling (LSP, terminal, etc.)
- Cross-platform
- Keyboard-centric

These philosophies align perfectly.

Setup gopls (Go Language Server)

-- ~/.config/nvim/after/plugin/lspconfig-go.lua

local lspconfig = require('lspconfig')

lspconfig.gopls.setup({
  cmd = { 'gopls', 'serve' },
  filetypes = { 'go', 'gomod', 'gowork', 'gotmpl' },
  root_dir = function(fname)
    local go_root = lspconfig.util.root_pattern('go.work', 'go.mod', '.git')(fname)
    return go_root
  end,
  on_attach = function(client, bufnr)
    local opts = { noremap = true, silent = true, buffer = bufnr }

    -- Go-specific keybindings
    vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
    vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts)
    vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts)
    vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)
    vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
    vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)
    vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, opts)
    vim.keymap.set('n', '<leader>f', vim.lsp.buf.format, opts)
    vim.keymap.set('n', '<leader>d', vim.diagnostic.open_float, opts)
    vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, opts)
    vim.keymap.set('n', ']d', vim.diagnostic.goto_next, opts)
  end,
  capabilities = require('cmp_nvim_lsp').default_capabilities(),
  settings = {
    gopls = {
      completeUnimported = true,
      usePlaceholders = true,
      analyses = {
        unusedparams = true,
        shadow = true,
        unusedwrite = true,
      },
      -- Enable semantic tokens
      semanticTokens = true,
      -- Enable code lenses
      codelens = {
        generate = true,
        test = true,
        gc_details = true,
      },
      hints = {
        assignVariableTypes = true,
        compositeLiteralFields = true,
        compositeLiteralTypes = true,
        constantValues = true,
        functionTypeParameters = true,
        parameterNames = true,
        rangeVariableTypes = true,
      },
    },
  },
})

-- Auto-format on save for Go
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*.go',
  callback = function()
    vim.lsp.buf.format({ async = false })
  end,
})

-- Run gofmt + goimports on save
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*.go',
  callback = function()
    local params = vim.lsp.util.make_range_params()
    params.context = { only = { 'source.organizeImports' } }
    local result = vim.lsp.buf_request_sync(0, 'textDocument/codeAction', params)
    for cid, res in pairs(result or {}) do
      for _, r in pairs(res.result or {}) do
        if r.edit then
          vim.lsp.util.apply_workspace_edit(r.edit, vim.lsp.get_client_by_id(cid).offset_encoding)
        else
          vim.lsp.buf.execute_command(r.command)
        end
      end
    end
  end,
})

Go Testing Integration

-- ~/.config/nvim/lua/go-testing.lua

local M = {}

-- Run current test file
function M.run_current_test_file()
  vim.cmd(':terminal go test -v ./%')
end

-- Run test under cursor
function M.run_test_under_cursor()
  local current_line = vim.fn.line('.')
  local current_file = vim.fn.expand('%')
  local test_name = vim.fn.matchstr(
    vim.fn.getline(current_line),
    'func \\(Test\\w*\\)'
  )
  if test_name ~= '' then
    vim.cmd(':terminal go test -v -run ' .. test_name .. ' ./' .. current_file)
  else
    vim.notify('Not in a test function', vim.log.levels.ERROR)
  end
end

-- Run all tests in package
function M.run_all_tests()
  vim.cmd(':terminal go test -v ./...')
end

-- Run tests with coverage
function M.run_tests_with_coverage()
  vim.cmd(':terminal go test -cover -coverprofile=coverage.out ./...')
  vim.cmd(':terminal go tool cover -html=coverage.out')
end

-- Benchmark current test
function M.run_benchmark()
  local current_line = vim.fn.line('.')
  local bench_name = vim.fn.matchstr(
    vim.fn.getline(current_line),
    'func \\(Benchmark\\w*\\)'
  )
  if bench_name ~= '' then
    vim.cmd(':terminal go test -bench ' .. bench_name .. ' -benchmem')
  else
    vim.notify('Not in a benchmark function', vim.log.levels.ERROR)
  end
end

-- Setup keybindings
vim.api.nvim_create_augroup('GoTesting', { clear = true })
vim.api.nvim_create_autocmd('FileType', {
  group = 'GoTesting',
  pattern = 'go',
  callback = function()
    local opts = { noremap = true, silent = true, buffer = true }
    vim.keymap.set('n', '<leader>gt', M.run_current_test_file, opts)
    vim.keymap.set('n', '<leader>gT', M.run_test_under_cursor, opts)
    vim.keymap.set('n', '<leader>ga', M.run_all_tests, opts)
    vim.keymap.set('n', '<leader>gc', M.run_tests_with_coverage, opts)
    vim.keymap.set('n', '<leader>gb', M.run_benchmark, opts)
  end,
})

return M

Usage:

<leader>gt  β†’ Run current test file
<leader>gT  β†’ Run test under cursor
<leader>ga  β†’ Run all tests in package
<leader>gc  β†’ Run tests with coverage
<leader>gb  β†’ Run benchmark

Go Debugging with DAP

-- ~/.config/nvim/lua/go-debug.lua

local dap = require('dap')
local dapui = require('dapui')

-- Configure Delve debugger for Go
dap.adapters.go = {
  type = 'executable',
  command = 'dlv',
  args = { 'dap' },
}

dap.configurations.go = {
  {
    type = 'go',
    name = 'Attach',
    mode = 'local',
    request = 'attach',
    processId = require('dap.utils').pick_process,
    showLog = false,
  },
  {
    type = 'go',
    name = 'Debug',
    mode = 'debug',
    request = 'launch',
    program = '${fileDirname}',
    env = {},
    args = {},
    showLog = false,
  },
  {
    type = 'go',
    name = 'Debug Test',
    mode = 'test',
    request = 'launch',
    program = '${fileDirname}',
    args = {},
    showLog = false,
  },
}

-- DAP UI setup
dapui.setup()

dap.listeners.after.event_initialized['dapui_config'] = function()
  dapui.open()
end
dap.listeners.before.event_terminated['dapui_config'] = function()
  dapui.close()
end
dap.listeners.before.event_exited['dapui_config'] = function()
  dapui.close()
end

-- Keybindings
vim.keymap.set('n', '<leader>db', dap.toggle_breakpoint, { noremap = true })
vim.keymap.set('n', '<leader>dc', dap.continue, { noremap = true })
vim.keymap.set('n', '<leader>dn', dap.step_over, { noremap = true })
vim.keymap.set('n', '<leader>di', dap.step_into, { noremap = true })
vim.keymap.set('n', '<leader>do', dap.step_out, { noremap = true })
vim.keymap.set('n', '<leader>dr', dap.repl.open, { noremap = true })

Go Project Structure Navigation

-- ~/.config/nvim/lua/go-nav.lua

-- Quick navigation for Go projects
vim.api.nvim_create_user_command('GoMain', function()
  vim.cmd(':edit main.go')
end, { desc = 'Open main.go' })

vim.api.nvim_create_user_command('GoTest', function()
  local current = vim.fn.expand('%:t:r')
  vim.cmd(':edit ' .. current .. '_test.go')
end, { desc = 'Open corresponding test file' })

vim.api.nvim_create_user_command('GoMod', function()
  vim.cmd(':edit go.mod')
end, { desc = 'Open go.mod' })

vim.api.nvim_create_user_command('GoSum', function()
  vim.cmd(':edit go.sum')
end, { desc = 'Open go.sum' })

-- Go to implementation (not just definition)
vim.api.nvim_create_user_command('GoImpl', function()
  vim.lsp.buf.implementation()
end, { desc = 'Go to interface implementation' })

-- View Go interfaces
vim.api.nvim_create_user_command('GoInterface', function()
  vim.cmd(':Telescope lsp_type_definitions')
end, { desc = 'Show interface definitions' })

Go Workflow Example

Scenario: Building a Kubernetes controller

You're editing: cmd/controller/main.go

Workflow:

1. Navigate to main.go
   :GoMain

2. View interface definition
   :GoInterface

3. Go to implementation
   :GoImpl

4. Run tests while editing
   <leader>gt   (runs go test -v)

5. Set breakpoint and debug
   <leader>db   (toggle breakpoint)
   <leader>dc   (continue execution)
   <leader>dn   (step over)

6. Benchmark performance-critical code
   <leader>gb   (run benchmarks)

7. Check code coverage
   <leader>gc   (run tests with coverage)

8. Format and organize imports on save
   Auto-runs goimports and gofmt

9. Commit changes
   <leader>gs   (git status)
   <leader>ga   (git add)
   <leader>gc   (git commit)

Advanced Go Analysis

-- ~/.config/nvim/lua/go-analysis.lua

-- Show type information
vim.api.nvim_create_user_command('GoType', function()
  vim.lsp.buf.hover()
end, { desc = 'Show type information' })

-- Show method receivers
vim.api.nvim_create_user_command('GoReceiver', function()
  local word = vim.fn.expand('<cword>')
  vim.cmd(':Telescope lsp_implementations')
end, { desc = 'Show method receivers' })

-- Inspect call hierarchy
vim.api.nvim_create_user_command('GoCallHierarchy', function()
  vim.lsp.buf.incoming_calls()
end, { desc = 'Show incoming calls' })

-- Generate interface implementation
vim.api.nvim_create_user_command('GoGenerateImpl', function()
  vim.cmd(':terminal go generate ./...')
end, { desc = 'Generate code with go generate' })

-- Run go vet
vim.api.nvim_create_user_command('GoVet', function()
  vim.cmd(':terminal go vet ./...')
end, { desc = 'Run go vet' })

-- Analyze goroutine leaks
vim.api.nvim_create_user_command('GoLeakCheck', function()
  vim.cmd(':terminal go test -run TestLeaks ./...')
end, { desc = 'Check for goroutine leaks' })

Go + DevOps Integration

Since DevOps increasingly uses Go:

-- ~/.config/nvim/lua/go-devops.lua

-- Build for multiple platforms
vim.api.nvim_create_user_command('GoBuildAll', function()
  vim.cmd(':terminal ' ..
    'GOOS=linux GOARCH=amd64 go build -o bin/app-linux && ' ..
    'GOOS=darwin GOARCH=amd64 go build -o bin/app-mac && ' ..
    'GOOS=windows GOARCH=amd64 go build -o bin/app.exe'
  )
end, { desc = 'Build for Linux, macOS, Windows' })

-- Build for ARM (Kubernetes nodes)
vim.api.nvim_create_user_command('GoBuildARM', function()
  vim.cmd(':terminal GOOS=linux GOARCH=arm64 go build -o bin/app-arm64')
end, { desc = 'Build for ARM64 (Kubernetes nodes)' })

-- Build Docker image
vim.api.nvim_create_user_command('GoDocker', function()
  vim.cmd(':terminal docker build -t myapp:latest .')
end, { desc = 'Build Docker image' })

-- Deploy Kubernetes
vim.api.nvim_create_user_command('GoDeployK8s', function()
  vim.cmd(':terminal kubectl apply -f k8s/ --dry-run=client -o yaml | kubectl apply -f -')
end, { desc = 'Deploy to Kubernetes' })

πŸš€ Conclusion: Why Neovim Wins for DevOps

Let me tie this together:

The Core Argument

DevOps work is fundamentally different from application development.

Application development:
- One project
- One language (usually)
- One IDE
- Local machine mostly
- Rich GUI helpful

DevOps work:
- Multiple infrastructure systems
- Multiple languages/tools
- SSH environments frequently
- Resource-constrained systems often
- GUI irrelevant in SSH terminal

Traditional IDEs (VS Code, JetBrains) were designed for application development. They’re being adapted for DevOps work, but that adaptation is uncomfortable.

Neovim was designed for exactly the scenario DevOps engineers face: editing code efficiently in terminals, over SSH, with minimal resources.

The Verdict

For DevOps engineers: Neovim is not just a choice. It's the rational choice.

Why?
1. Works perfectly in SSH (your primary work environment)
2. Minimal dependencies (reliability principle)
3. Fast (modal editing is optimized for speed)
4. Handles multiple languages (you use 5+ languages)
5. Scales to 100 servers (you can SSH to any one instantly)
6. Terminal-native (philosophy matches Unix philosophy)
7. Keyboard-centric (your hands never leave keyboard)
8. Highly customizable (your workflow, your config)

The Learning Path Forward

Week 1: Learn basic navigation
        (hjkl, i/esc, :w, :q)

Week 2: Learn efficient editing
        (word jumps, search, motions)

Week 3: Customize for your languages
        (LSP, formatters, keybindings)

Week 4: Integrate with your workflow
        (git, terraform, ansible)

Month 2+: Refine and optimize
         (you now prefer it to alternatives)

Result: You have a tool you'll use for 20 years

Starting Today

If you want to try Neovim for DevOps:

# Install Neovim
brew install neovim    # macOS
# or
apt install neovim     # Linux

# Copy the configuration from this guide
# ~/.config/nvim/init.lua

# Install plugin manager
git clone --depth 1 https://github.com/wbthomason/packer.nvim\
  ~/.local/share/nvim/site/pack/packer/start/packer.nvim

# Start Neovim
nvim

# Inside Neovim:
:PackerInstall

# Give it 20 hours
# You'll understand why DevOps engineers prefer it

The question isn’t whether Neovim is better than VS Code.

The question is: are you willing to invest 20 hours to have a tool specifically optimized for infrastructure work?

For most DevOps engineers, the answer should be yes.

Tags

#neovim #devops #editor #ssh #remote-development #infrastructure-as-code #workflow #productivity #terminal #configuration-management