lua-language-server icon indicating copy to clipboard operation
lua-language-server copied to clipboard

[Feature request]: Add ability to define enums without setting variable

Open lmburns opened this issue 2 years ago • 14 comments

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.

lmburns avatar Feb 20 '23 20:02 lmburns

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.

carsakiller avatar Feb 21 '23 02:02 carsakiller

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.

lmburns avatar Feb 21 '23 02:02 lmburns

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 avatar Feb 27 '23 11:02 C3pa

@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

lmburns avatar Feb 27 '23 21:02 lmburns

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: image

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.

C3pa avatar Feb 28 '23 07:02 C3pa

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

lmburns avatar Feb 28 '23 17:02 lmburns

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 avatar Feb 28 '23 18:02 carsakiller

@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.

lmburns avatar Mar 03 '23 17:03 lmburns

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: Animation 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?

C3pa avatar Apr 01 '23 11:04 C3pa

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

MarianoGnu avatar Aug 15 '25 00:08 MarianoGnu

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 ]])
Image

tomlau10 avatar Aug 17 '25 06:08 tomlau10

My enum:

Image

Warning in usage:

Image

MarianoGnu avatar Aug 18 '25 01:08 MarianoGnu

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.

CppCXY avatar Aug 18 '25 02:08 CppCXY

Thank you, this solved my issue <3

MarianoGnu avatar Aug 18 '25 14:08 MarianoGnu