Getting Started
Installation
VS Code
Install the wowlua-ls extension from the VS Code Marketplace. It bundles the language server binary - no separate install needed.
On editors that use the Open VSX Registry instead of the Marketplace (VSCodium, Cursor, Gitpod, Eclipse Theia), install it from there - it's the same extension.
JetBrains IDEs
Install WoW Lua Language Server from the JetBrains Marketplace (or Settings → Plugins → Marketplace, search for "WoW Lua"). Works in any JetBrains IDE 2025.2 or newer; it uses the LSP4IJ plugin as its LSP client, which the Marketplace installs automatically as a dependency. The plugin bundles the language server binary - no separate install needed.
Zed
Install WoW Lua Language Server from Zed's extensions view (zed: extensions). It runs the server set in lsp.wowlua-ls.binary.path, else a wowlua_ls on your $PATH, else downloads the release binary for your platform on first use - no separate install needed.
Zed gets its languages from extensions, so install two more:
- Lua - supplies the
Lualanguage and its grammar. Zed offers it the first time you open a.luafile. - wow-toc - supplies the
WoW TOClanguage. Without it, Zed treats.tocfiles as plain text and the server never sees them.
The Lua extension also brings LuaLS, which reports its own (WoW-unaware) diagnostics on the same files. Disable it for Lua buffers:
{
"languages": {
"Lua": {
"language_servers": ["wowlua-ls", "!lua-language-server", "..."]
}
}
}To run a server you built yourself that isn't on your $PATH, point the extension at it:
{
"lsp": {
"wowlua-ls": {
"binary": { "path": "/path/to/wowlua_ls" }
}
}
}Inlay hints are off by default in Zed; turn them on with "inlay_hints": { "enabled": true } under languages.Lua. Leave the top-level "code_lens" setting off: Zed renders the lenses, but the "N usages" and "overrides X" lenses run editor-side commands that only the VS Code and JetBrains clients implement, so clicking one does nothing.
Neovim
Neovim has built-in LSP client support - no plugin required, just configuration.
1. Get the binary. Either download a release from GitHub Releases, or build from source:
git clone https://github.com/TradeSkillMaster/wowlua-ls.git
cd wowlua-ls
cargo build --release
# Binary is at target/release/wowlua_ls2. Configure the LSP client. Add this to your Neovim config (requires Neovim 0.11+):
vim.filetype.add({ extension = { toc = 'toc' } })
vim.lsp.config('wowlua_ls', {
cmd = { '/path/to/wowlua_ls' },
filetypes = { 'lua', 'toc' },
root_dir = function(bufnr, on_dir)
on_dir(vim.fs.root(bufnr, '.wowluarc.json')
or vim.fs.root(bufnr, function(name) return name:match('%.toc$') ~= nil end)
or vim.fs.root(bufnr, '.git'))
end,
workspace_required = false,
})
vim.lsp.enable('wowlua_ls')Replace /path/to/wowlua_ls with the actual path to the binary. root_dir picks the addon folder: the nearest .wowluarc.json, else the folder holding the addon's .toc, else the git root. workspace_required = false lets the server attach to standalone Lua files outside a project. Neovim detects .toc files as a different filetype by default, so the vim.filetype.add line maps them to toc: the server only treats a file as a TOC file when its filetype is toc.
LazyVim
If you use LazyVim, add a plugin spec:
-- ~/.config/nvim/lua/plugins/wowlua-ls.lua
vim.filetype.add({ extension = { toc = "toc" } })
return {
{
"neovim/nvim-lspconfig",
opts = {
servers = {
wowlua_ls = {
cmd = { "/path/to/wowlua_ls" },
filetypes = { "lua", "toc" },
root_dir = function(bufnr, on_dir)
on_dir(vim.fs.root(bufnr, ".wowluarc.json")
or vim.fs.root(bufnr, function(name) return name:match("%.toc$") ~= nil end)
or vim.fs.root(bufnr, ".git"))
end,
workspace_required = false,
},
},
},
},
}nvim-lspconfig (standalone)
If you use nvim-lspconfig without a distro, you can register the server manually:
vim.filetype.add({ extension = { toc = 'toc' } })
local configs = require('lspconfig.configs')
if not configs.wowlua_ls then
configs.wowlua_ls = {
default_config = {
cmd = { '/path/to/wowlua_ls' },
filetypes = { 'lua', 'toc' },
root_dir = require('lspconfig.util').root_pattern('.wowluarc.json', '*.toc', '.git'),
single_file_support = true,
},
}
end
require('lspconfig').wowlua_ls.setup({})TIP
If you also use a general-purpose Lua language server (e.g. lua_ls), you may want to disable it for WoW addon projects to avoid duplicate diagnostics. You can do this by checking for .wowluarc.json in your lua_ls config's root_dir or using a filetype autocmd.
Other editors
Build the language server from source:
git clone https://github.com/TradeSkillMaster/wowlua-ls.git
cd wowlua-ls
cargo build --releaseThe binary is at target/release/wowlua_ls. Configure your editor to run it as an LSP server over stdio for Lua and TOC files. The server picks the file kind from the language ID the editor sends: lua for Lua files and toc for TOC files.
Your first project
Open a WoW addon folder in your editor. wowlua-ls will automatically:
- Scan all
.lua,.xml, and.tocfiles in the workspace - Load the built-in WoW API stubs (retail, classic, classic era, and WoW Forever)
- Resolve cross-file classes, globals, and addon namespaces
- Extract frame and template types from XML files
- Provide interactive editing support for TOC files
- Start reporting diagnostics
No configuration file is required to get started. The defaults are sensible for most addons.
Adding a configuration file
For project-specific settings, create a .wowluarc.json in your addon's root directory:
{
"ignore": ["Libs/"],
"flavors": ["retail", "classic"],
"diagnostics": {
"enable": ["need-check-nil"]
}
}This tells wowlua-ls:
- Skip
Libs/: don't analyze third-party library code - Target retail and classic: warn about flavor-specific APIs
- Enable nil checking: report
need-check-nilwarnings (off by default)
See the Configuration guide for more, and the Configuration reference for every setting.
Adding annotations
wowlua-ls infers a lot on its own, but annotations make it smarter. Start with the high-value ones:
Annotate your public API
---@param itemId number
---@param count number?
---@return boolean success
---@return string? error
function MyAddon:BuyItem(itemId, count)
if not self.store then
return false, "store not initialized"
end
-- ...
return true
endThe LS now knows the parameter types, that count is optional, and that the function returns a boolean plus an optional error string. Every caller gets type checking and hover information.
Annotate your classes
---@class AuctionEntry
---@field itemId number
---@field buyout number
---@field seller string
---@field duration number?Now AuctionEntry is a named type. You can reference it in @param and @return annotations across your codebase, and the LS will provide completion and type checking for its fields.
When to use @type
@type forces a variable's type. It's most useful when the LS can't infer what you need. For cases where the LS already has the answer, it's up to you: the annotation won't hurt, but you can save yourself the effort:
-- Useful: tells the LS what this empty table will hold
---@type AuctionEntry[]
local entries = {}
-- Useful: the LS can't infer the type from nil alone
---@type Frame?
local cachedFrame = nil
-- Optional: the LS already infers this from the assignment
---@type number
local x = 5What to annotate next
Once you've annotated your core classes and public functions, the LS handles most of the rest through inference. When you see a ? type on hover (meaning the LS couldn't figure it out), that's a signal to add an annotation.
