[Feature request]: Add ability to define enums without setting variable
How are you using the lua-language-server?
NeoVim
Which OS are you using?
Linux
What is the issue affecting?
Annotations
Expected Behaviour
Enum fields are defined and shown when hovering over the variable they are assigned to.
Actual Behaviour
Not all enum fields are picked up. The fields that are picked up are displayed as unknown.
Reproduction steps
Not needed for a feature request.
Additional Notes
When using Neovim, the standard library that comes with it is defined in files which use any a lot. An issue that I have ran into when trying to define an enum for a table that looks like the following is that it just won't work.
Table I am trying to define (this is what is show when hovering):
vim.log.levels: {
TRACE: integer = 0,
DEBUG: integer = 1,
INFO: integer = 2,
WARN: integer = 3,
ERROR: integer = 4,
OFF: integer = 5,
}
I have tried a couple of ways to define an enum to cover this:
-- Method 1
---@enum LogLevels
---| '0'
---| '1'
---| '2'
---| '3'
---| '4'
---| '5'
-- Method 2
---@enum LogLevels
---| TRACE = '0'
---| DEBUG = '1'
---| INFO = '2'
---| WARN = '3'
---| ERROR = '4'
---| OFF = '5'
-- Method 3
---@enum LogLevels
---| TRACE: 0
---| DEBUG: 1
---| INFO: 2
---| WARN: 3
---| ERROR: 4
---| OFF: 5
-- Method 4
---@enum LogLevels
---| TRACE: integer = 0 # Trace level
---| DEBUG: integer = 1 # Debug level
---| INFO: integer = 2 # Info level
---| WARN: integer = 3 # Warn level
---| ERROR: integer = 4 # Error level
---| OFF: integer = 5 # Off level
And then, when trying to set the variable to the enum vim.log.levels, I have done a couple of things as well:
-- Method 1
---@type LogLevels
M.levels = vim.log.levels --[=[@as LogLevels]=]
-- Method 2
---@type LogLevels
local tmp_levels = vim.log.levels --[=[@as LogLevels]=]
---@type LogLevels
M.levels = tmp_levels
When doing any of these, if I hover over M.levels only 3 of the fields are shown:
(field) M.levels: LogLevels {
ERROR: unknown,
INFO: unknown,
WARN: unknown,
}
I am aware that the following can be done to do something similar. However, I would like to be able to add descriptions to fields and I would like to not see a table literal in the inlayhints. Also, this won't work as a parameter type for a function because it would expect a table literal instead of an enum field.
This works for type declaration:
---@alias LogLevels { TRACE: 0, DEBUG: 1, INFO: 2, WARN: 3, ERROR: 4, OFF: 5 }
---@type LogLevels
M.levels = vim.log.levels
Log File
Not needed for a feature request.
I'm not too sure what you are looking for. If you are looking for how you can define an enum, you can do this:
---@enum logLevels
local levels = {
---Trace log level
TRACE = 0,
---Debug log level
DEBUG = 1,
---Info log level
INFO = 2,
---Warn log level
WARN = 3,
---Error log level
ERROR = 4,
---Off log level
OFF = 5,
}
---@param level logLevels
local function log(level) end
Going off the title of the issue, the point of an enum is to have a variable that contains some constants so that we can assign easy to read words to less understandable values at runtime.
If you want there to be no variable defined at runtime, you can do the following:
---@alias TRACE 0 Trace log level
---@alias DEBUG 1 Debug log level
---@alias INFO 2 Info log level
---@alias WARN 3 Warn log level
---@alias ERROR 4 Error log level
---@alias OFF 5 Off log level
---@alias LogLevels TRACE|DEBUG|INFO|WARN|ERROR|OFF
---@param level LogLevels
local function log(level) end
log(3) -- receive autocompletion for numbers
This provides some autocompletion and type checking, but I am not sure why you would use this over @enum.
The alias thing would work yes, but I am wanting to create an @enum to code that I do not have access to. The Neovim standard library is somewhat documented with types, but not all of it is, and not all of them are very specific.
If I go about creating an @enum like it says in the documentation, then I have to create a variable that I am not going to use. I guess I could create the variable for the enum and then later assign it to an underscore to remove the unused-local lint.
Whenever I am writing my own code that doesn't need access to Neovim's standard library, then I would actually create an enum with fields that I would later on be using. I just think that it would be more convenient to define things like this without actually creating a variable. The same way that you can create a @class without ever using it with any variable.
If I go about creating an @enum like it says in the documentation, then I have to create a variable that I am not going to use. I guess I could create the variable for the enum and then later assign it to an underscore to remove the unused-local lint.
Whenever I am writing my own code that doesn't need access to Neovim's standard library, then I would actually create an enum with fields that I would later on be using. I just think that it would be more convenient to define things like this without actually creating a variable. The same way that you can create a @class without ever using it with any variable.
Well, that's what the ---@alias annotation is used for. See the Literal Custom Type with Descriptions example.
@C3pa How would I use it then?
If I do what @carsakiller suggested when using aliases, I get an assign-type-mismatch warning.
---@alias TRACE 0 Trace log level
---@alias DEBUG 1 Debug log level
---@alias INFO 2 Info log level
---@alias WARN 3 Warn log level
---@alias ERROR 4 Error log level
---@alias OFF 5 Off log level
---@alias LogLevels TRACE|DEBUG|INFO|WARN|ERROR|OFF
---@type LogLevels
M.levels = vim.log.levels
Gives me this:
Cannot assign `table` to `0|1|2|3|4...(+1)`.
- `table` cannot match `0|1|2|3|4...(+1)`
- `table` cannot match any subtypes in `0|1|2|3|4...(+1)`
- Type `table` cannot match `5`
- Type `table` cannot match `4`
- Type `table` cannot match `3`
- Type `table` cannot match `2`
- Type `table` cannot match `1`
- Type `table` cannot match `0` [Lua Diagnostics. assign-type-mismatch]
If I use what I suggested, then a function expects a table and not a variant of an enum like what I would like. i.e.,
---@alias LogLevels { TRACE: 0, DEBUG: 1, INFO: 2, WARN: 3, ERROR: 4, OFF: 5 }
---@type LogLevels
M.levels = vim.log.levels
You can do it like this:
---@alias LogLevel
---| `0` # TRACE
---| `1` # DEBUG
---| `2` # INFO
---| `3` # WARN
---| `4` # ERROR
---| `5` # OFF
---@class myClass
---@field levels LogLevel
local M
M.levels = 2
Example:

You can get your descriptions to show on hover but you can't use anything such as vim.log.levels for tables defined in annotations. It can't work even conceptually if you remember that all the annotations start with -- which marks the beginning of a comment in Lua. How would the Lua interpreter read what you want from the annotations then?
The suggested solution allows you to have autocomplete suggestions for available constants that can be given to M.levels, and allow you to add a description of what each of them does.
That might be feasible. The thing is is that vim.log.levels is defined as a table, and I want M.levels to also be defined as a table (or @enum in this case). I want to use M.levels to access each enum variant (i.e., to be an enum) and not set M.levels to a single variant.
I still think that being able to define enums without actually using them would be more beneficial. This is how vim.log.levels is defined:
vim.log = {
levels = {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
},
}
Also, note that the main reason for me wanting to define enums without accessing them is because vim.log.levels is defined in a file which I am not able to edit. There are a lot of definitions under vim that are either annotated wrong, annotated as any, or don't have any annotations at all.
I don't know, maybe I'm using this wrong. It doesn't work how I would expect it to. If I do the following and just go ahead and define the enum myself like so:
---@enum LogLevels
local LogLevels = {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
OFF = 5
}
---@type LogLevels
M.levels = LogLevels
I get the following warning:
Cannot assign `table` to `LogLevels`.
- `table` cannot match `LogLevels`
- The object `table` cannot match the enumeration value of `LogLevels`. They must be the same object [Lua Diagnostics. assign-type-mismatch]
However, I am able to do the following:
---@enum LogLevels
M.levels = {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
OFF = 5
}
---@type LogLevels
vim.log.levels = vim.log.levels
-- But not this
---@type LogLevels
vim.log.levels = M.levels
Ideally, the documentation for the upstream code would be fixed. It makes this a pretty awkward use case. As well as all the other ways mentioned above, I have yet another possible solution 😄
---@class LogLevels
---@field TRACE 0 Trace log level
---@field DEBUG 1 Debug log level
M.levels = {}
---Interesting text
---@param level LogLevels
---@param msg string
function log(level, msg) end
log(M.levels.DEBUG)
Is your usage just that you overwrite vim.log.levels with your enum?
@carsakiller Yeah the @class method would work as well. I just haven't been able to use the @enum feature very much and would like to be able to use it if it makes more sense to do so.
Yes, my usage as of now is to just overwrite some things that I have defined to be more specific than the upstream code. Perhaps I could go through some of it and open a pull request for them. There are TONS of things that need to be fixed. So allowing an @enum in my configuration code for things that I don't have access to would allow me to modify the things that I would prefer to have better completion with.
I remembered how you could achieve something that looks like your original request:
---@alias vim.log.levels
---| `vim.log.levels.TRACE` # 0 Description for TRACE log level
---| `vim.log.levels.DEBUG` # 1 ...
---| `vim.log.levels.INFO` # 2
---| `vim.log.levels.WARN` # 3
---| `vim.log.levels.ERROR` # 4
---| `vim.log.levels.OFF` # 5
---@param level vim.log.levels
local function foo(level) end
Result:
Note: I don't have any vim libraries here (I am on VSCode), so that's why I got the "Undefined global vim." warning.
Does this do what you wanted?
I have been fighting with this for hours now, Lua annotations seems to not decide if enum exists or not. When it exists for annotation it doesn't exists for actual code and vice versa
https://github.com/user-attachments/assets/29e1caa7-9ab2-4e4c-956a-6e1464f60cec
The backtick style is called literal custom type (under @alias section: https://luals.github.io/wiki/annotations/#alias)
AFAIK, they are NOT actual types but only for triggering auto completion, and they have no type checking.
To specify a enum type, the best way is still using @enum on the table. 😕
If you have no access to that table / source code (because it's in some other library / upstream code), and you don't want to write a temp table in your file, you can write that table in a separate meta definition file: https://luals.github.io/wiki/definition-files/
- enum.d.lua (actually any filename is ok, as long as it is in your workspace / library path)
---@meta
---@enum TransitionType
TransitionType = {
None = 0, -- No Fade
Fade = 1, -- Fade
Slide = 2, -- Slide
Default = 0, -- Default is no Fade
}
- test.lua
---@param inTransition TransitionType
local function makeCurrent(inTransition) end
makeCurrent(--[[ try trigger completion here ]])
My enum:
Warning in usage:
My enum:
Warning in usage:
This isn’t an error from luals. You probably have other Lua plugins installed, such as luahelper. These plugins can make the color scheme look very ugly, and they aren’t actually compatible with luals.
Thank you, this solved my issue <3