Skip to main content

Quick Start Guide

This guide will get you from zero to a fully working EmmyLua Analyzer setup in just 5 minutes. Let’s dive in!
1

Install EmmyLua Analyzer

Install the language server using Cargo (Rust’s package manager):
cargo install emmylua_ls
Don’t have Rust? Install it first:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Or use pre-built binaries from the releases page.
2

Install Editor Extension

Install the appropriate extension for your editor:
Install the EmmyLua extension from the marketplace:
  1. Open VS Code
  2. Press Ctrl+P (or Cmd+P on macOS)
  3. Type: ext install tangzx.emmylua
  4. Press Enter
The extension will automatically detect and use your installed emmylua_ls binary.
3

Create a Lua Project

Create a new directory for your Lua project:
mkdir my-lua-project
cd my-lua-project
Create a simple Lua file main.lua:
-- Create a simple player system
local player = {
    name = "Hero",
    health = 100,
    position = {x = 0, y = 0}
}

function player:move(dx, dy)
    self.position.x = self.position.x + dx
    self.position.y = self.position.y + dy
    print(string.format("%s moved to (%d, %d)", 
        self.name, self.position.x, self.position.y))
end

function player:takeDamage(amount)
    self.health = self.health - amount
    if self.health <= 0 then
        print(self.name .. " has been defeated!")
    end
end

-- Test the player
player:move(10, 5)
player:takeDamage(30)
print("Health remaining: " .. player.health)
4

Add Type Annotations

Now let’s make the code better with EmmyLua annotations. Update main.lua:
---@class Player
---@field name string The player's name
---@field health number Current health points (0-100)
---@field position Position The player's position in world space
local Player = {}
Player.__index = Player

---@class Position
---@field x number X coordinate
---@field y number Y coordinate

---Create a new player instance
---@param name string The player's name
---@param health? number Starting health (defaults to 100)
---@return Player
function Player.new(name, health)
    local self = setmetatable({}, Player)
    self.name = name
    self.health = health or 100
    self.position = {x = 0, y = 0}
    return self
end

---Move the player by the specified delta
---@param dx number Change in x direction
---@param dy number Change in y direction
function Player:move(dx, dy)
    self.position.x = self.position.x + dx
    self.position.y = self.position.y + dy
    print(string.format("%s moved to (%d, %d)", 
        self.name, self.position.x, self.position.y))
end

---Apply damage to the player
---@param amount number Damage amount
---@return boolean alive True if player is still alive
function Player:takeDamage(amount)
    self.health = self.health - amount
    if self.health <= 0 then
        print(self.name .. " has been defeated!")
        return false
    end
    return true
end

---Get the distance from another position
---@param pos Position Target position
---@return number distance The Euclidean distance
function Player:distanceTo(pos)
    local dx = self.position.x - pos.x
    local dy = self.position.y - pos.y
    return math.sqrt(dx * dx + dy * dy)
end

-- Example usage
local hero = Player.new("Hero", 100)
hero:move(10, 5)

local alive = hero:takeDamage(30)
if alive then
    print("Health remaining: " .. hero.health)
end

local target = {x = 20, y = 10}
local distance = hero:distanceTo(target)
print(string.format("Distance to target: %.2f", distance))
5

Experience the Features

Open the file in your editor and try these features:
Type hero: and you’ll see:
  • move - Move the player by the specified delta
  • takeDamage - Apply damage to the player
  • distanceTo - Get the distance from another position
  • All fields: name, health, position
The completion includes type information and documentation!
Hover over:
  • Player.new → See the function signature and documentation
  • hero → See it’s type Player with all fields
  • self.health → See it’s type number and description
  • Ctrl+Click (or Cmd+Click) on Player.new to jump to its definition
  • Works across files in your project
  • Jump back with Alt+Left Arrow
  • Right-click on Player and select “Find All References”
  • See everywhere the class is used
  • Works for functions, variables, and types
Try introducing errors:
hero:move("wrong", "types")  -- Error: param type not match
hero.nonexistentField = 5     -- Warning: undefined field
local unused = 42             -- Hint: unused variable
EmmyLua will highlight these issues immediately!
  • Right-click on Player and select “Rename Symbol”
  • Type a new name like Character
  • All references are updated across your project!

Configure Your Project (Optional)

Create a .emmyrc.json file in your project root for custom configuration:
{
  "$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json",
  "runtime": {
    "version": "Lua5.4"
  },
  "diagnostics": {
    "enable": true,
    "globals": ["love", "vim"],
    "severity": {
      "unused": "hint",
      "undefined-global": "warning"
    }
  },
  "completion": {
    "enable": true,
    "callSnippet": true,
    "autoRequire": true
  },
  "hint": {
    "enable": true,
    "paramHint": true,
    "localHint": true
  }
}
The $schema field enables auto-completion for the config file itself!

Advanced Example: Generic Functions

Let’s create a more advanced example with generics:
---A generic container for storing items
---@class Container<T>
---@field private items T[]
---@field capacity number Maximum number of items
local Container = {}
Container.__index = Container

---Create a new container
---@generic T
---@param capacity number Maximum capacity
---@return Container<T>
function Container.new(capacity)
    local self = setmetatable({}, Container)
    self.items = {}
    self.capacity = capacity
    return self
end

---Add an item to the container
---@generic T
---@param item T The item to add
---@return boolean success True if item was added
function Container:add(item)
    if #self.items >= self.capacity then
        return false
    end
    table.insert(self.items, item)
    return true
end

---Get all items matching a predicate
---@generic T
---@param predicate fun(item: T): boolean Filter function
---@return T[] matches Items matching the predicate
function Container:filter(predicate)
    local result = {}
    for _, item in ipairs(self.items) do
        if predicate(item) then
            table.insert(result, item)
        end
    end
    return result
end

---Map items to a new type
---@generic T, R
---@param mapper fun(item: T): R Transform function
---@return Container<R> newContainer Container with transformed items
function Container:map(mapper)
    local result = Container.new(self.capacity)
    for _, item in ipairs(self.items) do
        result:add(mapper(item))
    end
    return result
end

-- Example usage with full type inference
local numbers = Container.new(10)  ---@type Container<number>
numbers:add(1)
numbers:add(2)
numbers:add(3)

-- Filter even numbers
local evens = numbers:filter(function(n)
    return n % 2 == 0
end)

-- Map to strings
local strings = numbers:map(function(n)
    return tostring(n)
end)  -- Type: Container<string>

print("Container demo complete!")
Notice how EmmyLua tracks the generic types through the filter and map operations, providing accurate type information at every step!

Run Static Analysis

Optionally install the static analyzer to check your code:
cargo install emmylua_check
Then analyze your project:
emmylua_check .
You’ll see a report of any issues found:
✓ Analysis complete
  Found 0 errors, 0 warnings in 2 files
  Analyzed in 143ms

Generate Documentation

Optionally install the documentation generator:
cargo install emmylua_doc_cli
Generate beautiful documentation from your annotations:
emmylua_doc_cli . -o ./docs --site-name "My Lua Project"
This creates Markdown documentation in the ./docs directory!

Common Use Cases

Game Development

Perfect for Lua-based game engines like:
  • LÖVE 2D
  • Defold
  • Corona SDK
  • Roblox
Get type-safe APIs and instant feedback.

Neovim Configuration

Write better Neovim configs with:
  • Full vim API completion
  • Type checking for plugins
  • Documentation on hover

Web Development

Use with:
  • OpenResty / Nginx
  • Lapis framework
  • API development
Catch errors before deployment.

Embedded Scripting

Enhance embedded Lua in:
  • Application plugins
  • Configuration systems
  • Scripting engines

Troubleshooting

Check that emmylua_ls is in your PATH:
which emmylua_ls  # macOS/Linux
where emmylua_ls  # Windows
If not found, ensure ~/.cargo/bin is in your PATH:
export PATH="$HOME/.cargo/bin:$PATH"
  • Make sure the file is saved with a .lua extension
  • Check your editor’s LSP status (usually in the status bar)
  • Restart the language server from your editor’s command palette
  • Check for errors in the language server logs
  • Ensure you’re using the correct annotation syntax: ---@class, ---@param, etc.
  • Check that there are no syntax errors in your annotations
  • The annotation must be directly above the code it describes
  • No blank lines between annotation and code
  • Make sure you’ve opened the project root folder in your editor, not just a file
  • Check that .emmyrc.json is in the project root
  • Try reloading the workspace

Next Steps

Editor Setup

Detailed configuration for your specific editor

Annotation Guide

Learn all the annotation types and their uses

Configuration

Customize EmmyLua for your workflow

Static Analysis

Integrate checks into your CI/CD pipeline

Documentation

Generate docs from your annotations

Contributing

Learn how to contribute to the project

Community Resources

GitHub Discussions

Ask questions and share your projects

Report Issues

Found a bug? Let us know!
Congratulations! You’re now ready to write better Lua code with EmmyLua Analyzer. Happy coding!

Build docs developers (and LLMs) love