How-to

How to set up an Apex language server in Neovim

Salesforce Apex tooling has always assumed you use VS Code. It does not have to: Nimbus ships an LSP 3.17 server for Apex in the same binary as the CLI, so any editor that speaks the protocol gets completion, hover, go-to-definition, and SOQL column validation. Neovim, Zed, and Helix configs below.

Short version

The server command is nimbus lsp. Your editor needs three things: that command, a filetype mapping for .cls and .trigger, and sfdx-project.json as the root marker. Everything else is editor-specific syntax.

Three things every LSP client needs

Language server setup looks different in every editor and is the same three decisions underneath. Getting them explicit makes the configs below readable rather than copy-paste magic:

  • How to start the server. A command the editor launches and talks to over stdin/stdout. Here: nimbus lsp.
  • Which files it owns. Apex lives in .cls and .trigger files. No editor detects those as Apex out of the box — this is the step people skip, and then the server never starts.
  • Where the project root is. sfdx-project.json. The server resolves classes, triggers, and the schema cache relative to it.

Install

The language server is not a separate download — it is a subcommand of the CLI:

bash
# macOS / Linux
curl -sSL https://testnimbus.dev/install.sh | bash

# Windows (PowerShell)
iwr -useb https://testnimbus.dev/install.ps1 | iex

# Confirm it is on PATH
nimbus --version

Neovim 0.11+ (native LSP)

Neovim 0.11 and later has native LSP configuration. Drop this into your config:

lua
-- ~/.config/nvim/init.lua (or any loaded file)
vim.lsp.config.nimbus = {
  cmd = { 'nimbus', 'lsp' },
  filetypes = { 'apex', 'apexcode' },
  root_markers = { 'sfdx-project.json' },
  -- Optional: write a protocol trace for debugging
  -- cmd = { 'nimbus', 'lsp', '--log', '/tmp/nimbus-lsp.log' },
}
vim.lsp.enable('nimbus')

-- .cls and .trigger don't have a built-in filetype; teach Neovim:
vim.filetype.add({
  extension = {
    cls = 'apex',
    trigger = 'apex',
  },
})

Neovim 0.8–0.10 (nvim-lspconfig)

On older Neovim, register the server as a custom lspconfig entry:

lua
require('lspconfig.configs').nimbus = {
  default_config = {
    cmd = { 'nimbus', 'lsp' },
    filetypes = { 'apex' },
    root_dir = require('lspconfig.util').root_pattern('sfdx-project.json'),
    settings = {},
  },
}
require('lspconfig').nimbus.setup({})

The vim.filetype.add block from the previous section is still required — lspconfig matches on filetype, and nothing maps .cls to apex for you.

Zed

Zed configures language servers per language in settings.json:

json
// ~/.config/zed/settings.json
{
  "lsp": {
    "nimbus": {
      "binary": { "path": "nimbus", "arguments": ["lsp"] }
    }
  },
  "languages": {
    "Apex": { "language_servers": ["nimbus"] }
  },
  "file_types": {
    "Apex": ["cls", "trigger"]
  }
}

Helix

Helix wires everything through languages.toml — server, file types, and roots in one place:

toml
# ~/.config/helix/languages.toml
[language-server.nimbus-lsp]
command = "nimbus"
args = ["lsp"]

[[language]]
name = "apex"
scope = "source.apex"
file-types = ["cls", "trigger"]
roots = ["sfdx-project.json"]
language-servers = ["nimbus-lsp"]

What you get once it connects

The server advertises its capabilities at initialize; how much of that renders depends on your client. The core is what you would expect — diagnostics on parse errors, completion, hover, signature help, go-to-definition, find references, rename, document symbols, folding, call hierarchy — plus two things specific to running Apex locally:

  • Live SOQL column validation. [SELECT Namee FROM Account] is a warning at the point you type it, with a quick fix offering the nearest real field names — including relationship fields once the chain resolves to a synced object.
  • Platform-data overlays. Coverage hit counts, surviving mutants, and flakiness rates from your last nimbus test run, surfaced as inlay hints and hover text.

All of it resolves against the local .nimbus/schemas/ cache rather than an org, so completion works offline and does not wait on a network round-trip. The full capability list is on the language server page.

When it does not work

  • The server never starts. Confirm the binary resolves from the editor's environment (nimbus --version), then run nimbus lsp --log /tmp/nimbus-lsp.log by hand. If the log stays empty when the editor should be talking to it, the editor is not finding the binary.
  • No diagnostics in a .cls file. Check the buffer's filetype. This is the filetype mapping step, and Neovim in particular does not auto-detect .cls.
  • Custom objects missing from completion. Run nimbus sync once to populate .nimbus/schemas/. Standard objects ship with the binary; yours do not.
  • Completion empty after a variable. The server needs the declared type. If the variable is declared in another file, use the fully-qualified name for now — cross-file type resolution is not fully wired.

Next steps

Once the server is answering, the rest of the loop is terminal-side and editor-agnostic: nimbus test "*" to run the suite, nimbus exec for a scratch snippet. Nothing about it assumes a GUI IDE.