Skip to content

No IntelliSense for callbacks - named functions. #2368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Poltergeist-ix opened this issue Oct 13, 2023 · 12 comments
Open

No IntelliSense for callbacks - named functions. #2368

Poltergeist-ix opened this issue Oct 13, 2023 · 12 comments
Labels
feat/LuaCats Annotations Related to Lua Language Server Annotations (LuaCats) feat/type check Related to the type checking feature

Comments

@Poltergeist-ix
Copy link

How are you using the lua-language-server?

Visual Studio Code Extension (sumneko.lua)

Which OS are you using?

Windows

What is the issue affecting?

Other

Expected Behaviour

Function would have Intellisense like the anonymous function in picture.
pic1-Intellisense

Actual Behaviour

No Intellisense for function.
pic1-no-IntelliSense

Reproduction steps

none

Additional Notes

No response

Log File

No response

@carsakiller carsakiller added feat/LuaCats Annotations Related to Lua Language Server Annotations (LuaCats) feat/type check Related to the type checking feature labels Oct 13, 2023
@carsakiller
Copy link
Collaborator

Do you have a definition file that defines what Events.OnTriggerNPCEvent.Add() accepts? It appears that you do. You would probably want to make the callback an @alias so that you can tell the language server the expected signature of the function:

---@alias NPC_Event_Callback fun(type: string, data: table, def: BuildingDef): any

---@type NPC_Event_Callback
local function triggered() end

It can then be used for your definition of Events.OnTriggerNPCEvent.Add() as well:

---@param callback NPC_Event_Callback
function Events.OnTriggerNPCEvent.Add(callback) end

@Poltergeist-ix
Copy link
Author

Thank you, this works. Only issue with example is to use the alias as param name or in comment for better visibility.

---@param NPC_Event_Callback NPC_Event_Callback
function Events.OnTriggerNPCEvent.Add(NPC_Event_Callback) end

@Poltergeist-ix
Copy link
Author

Has this been fixed to show automatically or is it not a planned feature?

@carsakiller
Copy link
Collaborator

Sorry, I thought you meant the only issue you had with my example was that the parameter was named different from the alias. Do you want the parameter to automatically receive the name of the alias? I'm not sure what you mean.

Maybe you mean expandAlias?

@Poltergeist-ix
Copy link
Author

I was wondering if it was possible to achieve this automatically, without adding an alias at all.

@carsakiller
Copy link
Collaborator

I don't believe so. The anonymous function is receiving completion because Events.OnTriggerNPCEvent.Add() is describing its callback parameter. When you define the function separately, there is no association with Events.OnTriggerNPCEvent.Add().

I'm not really sure if this can be automatically supported because when you are writing the function separately like this, you are defining it right then and there, therefor its signature is as it is defined. Either way, I'll reopen this for sumneko to decide whether this is something that can be done.

@carsakiller carsakiller reopened this Oct 24, 2023
@Foereaper
Copy link

This would be a great addition, our Lua interpreter is primarily event hooks and callbacks, with separate function definitions being the norm.

I doubt we'll be able to get our end users to change their behavior, and having to manually define the argument types kind of defeats the purpose when we have a bunch of variadic events and arguments per event type.

@tomlau10
Copy link
Contributor

Hi @Poltergeist-ix @Foereaper, this is partially achieved in my recent PR #2946, by interpreting the callback params type according to where the callback is used. You have to enable Lua.type.inferParamType config to use this feature.

Events = {}

---@class BuildingDef

---@param callback fun(type: string, data: table, def: BuildingDef)
function Events.OnTriggerNpcEvent.Add(callback) end

local function triggered(type, data, def)
    --> type: string, data: table, def: BuildingDef
end
Events.OnTriggerNpcEvent.Add(triggered)

Note that this doesn't include the param name auto completion, you still have to manually type the param names in your callback though. In the above example, if the local function triggered only has one param named a, then its signature preview will just be fun(a: string).

@Foereaper
Copy link

Great, that seems to work pretty well actually, thanks! That said though, in our case, the callback function parameter signature varies depending on the event ID variable provided in the register function. I reckon this would partially be possible with #1456, but I haven't figured out a good way to do this with non-classed methods (ie. globals). Example definition (with the above callback function signature) below:

---@param event number Player event Id, refer to PlayerEvents above. Valid numbers: integers from 0 to 4,294,967,295.
---@param func fun(event: integer, player: Player) Function to register.
---@param shots? number Default value: (0) The number of times the function will be called, 0 means "always call this function". Valid numbers: integers from 0 to 4,294,967,295.
---@return function cancel A function that cancels the binding when called.
function RegisterPlayerEvent(event, func, shots) end

As an example, event 1 would be callback signature: event, player
Whereas event 13 would be callback signature: event, player, level (int)

Ideas and/or thoughts?

@tomlau10
Copy link
Contributor

tomlau10 commented Nov 29, 2024

I reckon this would partially be possible with #1456
...
As an example, event 1 would be callback signature: event, player
Whereas event 13 would be callback signature: event, player, level (int)

If you want different event enums to have different signature callbacks, then you may write some @overload annotation for your register function. @Foereaper

Method 1

  • meta.lua
---@meta

---@class Player

---@param event integer Player event Id, refer to PlayerEvents above. Valid numbers: integers from 0 to 4,294,967,295.
---@param func fun(event: integer, player: Player) Function to register.
---@param shots? integer Default value: (0) The number of times the function will be called, 0 means "always call this function". Valid numbers: integers from 0 to 4,294,967,295.
---@return function cancel A function that cancels the binding when called.
---@overload fun(event: 1, func: fun(event: 1, player: Player), shots?: integer): function
---@overload fun(event: 13, func: fun(event: 13, player: Player, level: integer), shots?: integer): function
function RegisterPlayerEvent(event, func, shots) end
  • test.lua
local function event1cb(event, player)
    --> event: 1, player: Player
end
RegisterPlayerEvent(1, event1cb)

local function event13cb(event, player, level)
    --> event: 13, player: Player, level: integer
end
RegisterPlayerEvent(13, event13cb)
  • LuaLS should be capable to type narrow the function call based on the literal enums (although this function type matching feature may not be perfect all the time)
  • Actually for classes I think it is more correct to use @overload on the method instead of @field on the class itself 😂

Method 1.5 with alias

Furthermore, you may use alias for better readability, as suggested in this discussion: #2723 (comment)
Then you may have something like this in your meta definition file:

---@meta

---@class Player

---@alias EventId.EVENT_A 1
---@alias EventCb.EVENT_A fun(event: EventId.EVENT_A, player: Player)

---@alias EventId.EVENT_B 13
---@alias EventCb.EVENT_B fun(event: EventId.EVENT_B, player: Player, level: integer)

---@param event integer Player event Id, refer to PlayerEvents above. Valid numbers: integers from 0 to 4,294,967,295.
---@param func fun(event: integer, player: Player) Function to register.
---@param shots? integer Default value: (0) The number of times the function will be called, 0 means "always call this function". Valid numbers: integers from 0 to 4,294,967,295.
---@return function cancel A function that cancels the binding when called.
---@overload fun(event: EventId.EVENT_A, func: EventCb.EVENT_A, shots?: integer): function
---@overload fun(event: EventId.EVENT_B, func: EventCb.EVENT_B, shots?: integer): function
function RegisterPlayerEvent(event, func, shots) end
  • But unfortunately you cannot have description for each of your callback signature using @overload method ...

Method 2 with multiple definition

When writing meta definition file, the official docs suggested that you can have multiple definition for the same function. So you may come up with something like this discussion comment: #2723 (reply in thread)

---@meta

---@class Player

---@param event integer Player event Id, refer to PlayerEvents above. Valid numbers: integers from 0 to 4,294,967,295.
---@param func fun(event: integer, player: Player) Function to register.
---@param shots? integer Default value: (0) The number of times the function will be called, 0 means "always call this function". Valid numbers: integers from 0 to 4,294,967,295.
---@return function cancel A function that cancels the binding when called.
function RegisterPlayerEvent(event, func, shots) end

---@alias EventId.EVENT_A 1
---@alias EventCb.EVENT_A fun(event: EventId.EVENT_A, player: Player)
---@param event EventId.EVENT_A Event A
---@param func EventCb.EVENT_A Event A callback
---@param shots? integer Default value: (0) The number of times the function will be called, 0 means "always call this function". Valid numbers: integers from 0 to 4,294,967,295.
---@return function cancel A function that cancels the binding when called.
function RegisterPlayerEvent(event, func, shots) end

---@alias EventId.EVENT_B 13
---@alias EventCb.EVENT_B fun(event: EventId.EVENT_B, player: Player, level: integer)
---@param event EventId.EVENT_B Event B
---@param func EventCb.EVENT_B Event B callback
---@param shots? integer Default value: (0) The number of times the function will be called, 0 means "always call this function". Valid numbers: integers from 0 to 4,294,967,295.
---@return function cancel A function that cancels the binding when called.
function RegisterPlayerEvent(event, func, shots) end
  • But then the downside is description for common params are not shared... You have to duplicate it each time you define the function
  • Moreover the goto definition in this method seems not that good, because luals thinks that there are multiple definition of this function, and cannot jump correctly based on the type narrowed result 😕

@Foereaper
Copy link

Thank you for the great breakdown. I could have sworn I tested method 1, but struggled with the function args showing "any". Your example works perfectly though, so now I can rewrite my parser again :)

@tomlau10
Copy link
Contributor

but struggled with the function args showing "any".

The param infer for named function is only added recently in v3.13.0.
Before that it only works for anonymous function 😄

RegisterPlayerEvent(1, function (event, player)
    --> event: 1, player: Player
end)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat/LuaCats Annotations Related to Lua Language Server Annotations (LuaCats) feat/type check Related to the type checking feature
Projects
None yet
Development

No branches or pull requests

4 participants