This is a plugin for the sumneko.lua vscode extension to help with factorio related syntax and intelisense.
To keep up with this project check the changelog.
To use this plugin clone this repository into your .vscode/lua
folder:
- If you don't already have git installed, download it from here.
- In vscode right click on your
.vscode
folder and clickOpen in Integrated Terminal
. - In the terminal run:
git clone --single-branch --branch master https://github.com/JanSharp/FactorioSumnekoLuaPlugin.git lua
This will clone the master branch of this repository into the lua
folder from the current directory, which is the .vscode
directory.
Since sumneko.lua
2.0.0 Lua.runtime.plugin
defaults to ""
instead of ".vscode/lua/plugin.lua"
so you'll have to configure this setting to ".vscode/lua/plugin.lua"
yourself. Best would be to do this per workspace for security.
After installing make sure to reload vscode.
To update the plugin simply use git pull
. The master branch should always be in a functional state.
- In vscode right click on your
.vscode/lua
folder and clickOpen in Integrated Terminal
. - In the terminal run:
git pull
Or use any other method of using git you're comfortable with.
After updating make sure to reload vscode.
If you happen to have a different setup and cannot put the repository in its default location, clone it to wherever you want (the folder does not have to be called lua
anymore at that point) and then configure the Lua.runtime.plugin
setting. The file name of the plugin entrypoint is plugin.lua
. It can be a relative path from the root of the workspace directory. (Best would be to do this per workspace for security.)
If you're new to command line programs and you cannot use the Open in Integrated Terminal
in your case, simply use the cd
"command" (i think it's called) in any command line to navigate to the directory you want to clone the repository into.
For example open a command line window or terminal of some kind (on windows i'd use git bash
which comes with git. Just search for it in the start menu).
cd C:/dev/factorio/modding
git clone --single-branch --branch master https://github.com/JanSharp/FactorioSumnekoLuaPlugin.git
And to update:
cd C:/dev/factorio/modding/FactorioSumnekoLuaPlugin
git pull
(git bash doesn't like back slashes)
And if the workspace is at C:/dev/factorio/modding
the Lua.runtime.plugin
would be set to FactorioSumnekoLuaPlugin/plugin.lua
, most likely as a workspace setting, not system wide setting.
If the plugin is causing the language server to report syntax errors when there really aren't any and you need a temporary "solution" before reporting the issue and waiting for a fix simply put --##
at the very very start of the file. If it is a problem that it has to be at the very start of the file please create an issue with a reason/an example for it.
What the plugin fundamentally does is make the lua extension (to which i'll refer to as lua language server) think files look different than they actually do. This allows for the language server to understand custom syntax, which factorio doesn't have a lot of, but it does help with a few things.
In factorio to require files from other mods you use
require("__modname__.filename")
however the folder __modname__
does not exist, which means the language server cannot find the file and cannot assist you with any kind of intelisense, mainly to know what the file returns and to navigate to definitions and find references.
The plugin makes these look like this to the language server
require("modname.filename")
That means if there is a folder with the name modname
it can now find the files.
(This might get improved to support folders with version numbers at the end. Zips may currently not even be possible with the extension, however it might get supoort at some point too.)
the module paths passed to require
also get normilzed to follow this.kind.of.pattern
, because it is the only pattern currently supported by the language server for it to locate files.
For example
require("folder.foo")
require("folder/bar")
require("folder/baz.lua")
Would look like this to the language server
require("folder.foo")
require("folder.bar")
require("folder.baz")
If the language server sees multiple mods it can happen that it thinks your global
contains keys/data it really doesn't because some other mod stores said data in global. For that reason the plugin tries its best to make global
look like __modname__global
to the language server.
In order to not touch the wrong things it only replaces those where global
is followed by .
(a dot), [
(open square bracket) or =
(equals).
When using generated EmmyLua docs for the Factorio API from the JSON docs (such as mentioned in this section) the plugin can help reduce how many type annotating you have to write manually by automatically adding type annotations for event handler parameters withing script.on_event
calls (or the other 2 variants from flib or Stdlib). This also works with an array of event names.
If you ever want or need this to be disabled for a specific event handler put --##
somewhere after the parameter name of the handler but before the end of line. This may be required when annotating custom event handlers, see in the examples below.
The specific behavior is hard to put in words but i shall try:
For all calls to on_event
, event.register
or Event.register
it gets all event names a handler is being registered for which is either just the single one provided or the list of event "names". It then gets the parameter name used for the event data in the handler function provided (it only works when the function is defined in place, not for references to a previously defined function) and it adds an @param
annotation for this parameter. For every event name previously found it tries to get the type name to use for this annotation by getting the last part in the indexing chain/expression and combines then with a |
between each of them to tell the language server that it could be any of those given types, but it will only use the types that start with on_
or script_
.
flib
and stdlib
add another way of registering handlers, such as event.on_tick(function(e) end)
. These are much easier to explain:
It searches for event.
or Event.
followed by an identifier which gets called with a function being passed in as the first argument. Then it adds the annotaion just as before by getting the parameter name used for the event data for the handler and adds an @param
annotation for this parameter using the found function name (the identifier after event.
or Event.
) as the type name for the parameter without any further filtering on the name.
It doesn't do anything if it finds --
somewhere in the line before whichever call it is processing.
It disables undefined-doc-name
diagnostics on the @param
annotation line because it can find false positives or one might not be using the generated EmmyLua docs.
For example
script.on_event(defines.events.on_tick, function(event)
print("Hello World!")
end)
event.register(defines.events.on_built_entity, function(e) end)
Event.on_built_entity(function(e) end)
Would look something similar to this to the language server
script.on_event(defines.events.on_tick,
---@diagnostic disable-next-line:undefined-doc-name
---@param event on_tick
function(event)
end)
event.register(defines.events.on_built_entity,
---@diagnostic disable-next-line:undefined-doc-name
---@param e on_built_entity
function(e) end)
Event.on_built_entity(
---@diagnostic disable-next-line:undefined-doc-name
---@param e on_built_entity
function(e) end)
For example
script.on_event({
defines.events.script_raised_built,
defines.events.on_built_entity,
}, function(event)
end)
event.register({
defines.events.script_raised_built,
defines.events.on_built_entity,
}, function(e) end)
Would look something similar to this to the language server
script.on_event({
defines.events.script_raised_built,
defines.events.on_built_entity,
},
---@diagnostic disable-next-line:undefined-doc-name
---@param event script_raised_built|on_built_entity
function(event)
end)
event.register({
defines.events.script_raised_built,
defines.events.on_built_entity,
},
---@diagnostic disable-next-line:undefined-doc-name
---@param e script_raised_built|on_built_entity
function(e) end)
For example
script.on_event("on_tick", function(event)
end)
script.on_event(on_custom_event, function(event)
end)
---@param event my_on_custom_event_type
script.on_event(on_custom_event, function(event) --##
end)
Would look something similar to this to the language server
script.on_event("on_tick", function(event)
end)
script.on_event(on_custom_event,
---@diagnostic disable-next-line:undefined-doc-name
---@param event on_custom_event
function(event)
end)
---@param event my_on_custom_event_type
script.on_event(on_custom_event, function(event) --##
end)
To help with intelisense for remotes, such as go to definition or knowing about which parameters a remote interface function takes and what it returns the plugin makes remote.call
and remote.add_interface
calls look different to the language server.
For example
remote.add_interface("foo", {
---Hello World!
---@param hello string
---@param world string
---@return number
bar = function(hello, world)
return 42
end,
})
remote.call("foo", "bar", "arg 1", "arg 1")
Would look something similar to this to the language server
remote.__all_remote_interfaces.foo = {
---Hello World!
---@param hello string
---@param world string
---@return number
bar = function(hello, world)
return 42
end,
}
remote.__all_remote_interfaces.foo.bar("arg 1", "arg 2")
Then when you for example hover over the string "bar"
in the remote.call
call you should get intelisense showing the signature of the function bar as defined above.
It also disables undefined-field
diagnostics specifically for __all_remote_interfaces
and does nothing if it finds --
before remote
on the same line.
If you payed close attention to the previous example you may notice that the remote.add_interface
replacement has to remove the closing )
(parenthesis) of the call. In order to find this parethesis it's using %b()
in a pattern, which means it can fail to find the right parenthesis if there are unbalanced or escaped parenthesis inside strings or comments. You can either manually add parenthesis inside comments to balance them out again, or if it's just not worth it you can add --##
somewhere within or after the remote.add_interface
call, but the earlier the better, because it will only search for it until the end of the line where it found its closing parenthesis.
Here are some examples
remote.add_interface("foo", {
bar = function()
return ")"
end,
})
remote.add_interface("foo", {
bar = function() -- ( for plugin
return ")"
end,
})
remote.add_interface("foo", { --## plugin, don't even try
bar = function()
return "())(((()())(())()))())"
end,
})
local foo = {
bar = function()
return "())(((()())(())()))())"
end,
}
remote.add_interface("foo", foo)
Would look something similar to this to the language server (notice the strings)
remote.__all_remote_interfaces.foo = {
bar = function()
return ""
end,
})
remote.__all_remote_interfaces.foo = {
bar = function() -- ( for plugin
return ")"
end,
}
remote.add_interface("foo", { --## plugin, don't even try
bar = function()
return "())(((()())(())()))())"
end,
})
local foo = {
bar = function()
return "())(((()())(())()))())"
end,
}
remote.__all_remote_interfaces.foo = foo
The language server is getting better support for EmmyLua annotations, but it was really missing a way to define multiple types on the same line. For example for functions that return multiple values.
For example
---@typelist integer, string
local foo, bar = string.match("Hello world!", "()(l+)")
Would look something similar to this to the language server
---@type integer
local foo,
---@type string
bar = string.match("Hello world!", "()(l+)")
It only supports ---@typelist
(with spaces allowed between ---
and @typelist
) being on one line and it only affects the next line. And it uses ,
(commas) as separators. (commas inside < >
or ( )
are ignored on the ---@typelist
line.)
Important: Does not work in sumneko.lua
2.4.0
or later and there is currently no other known workaround
Another thing the annotations are lacking currently is a way to change the type of a variable, which is usually something you want to narrow down the type of that variable.
For example
---@param value any
local function foo(value)
if type(value) == "string" then
---@narrow value string
-- now value is a string, not any
end
end
Would look something similar to this to the language server
---@param value any
local function foo(value)
if type(value) == "string" then
value = nil ---@type string
-- now value is a string, not any
end
end
It specifically looks for ---@narrow
(with spaces allowed between ---
and @narrow
) followed by space and an identifier, then does the replacement so that the type is actually used in place, exactly how/where you wrote it.
Unfortunately since it is using nil
as a placeholder assignment the language server will think the variable can be nil
even though it might never be. An expression the language server resolves to any
would be better, but i don't know of one right now.