I have been doing all my Luau development locally on my Mac for a week or so now. Let me describe my personal development environment, and work a bit on improving it.
I edit in Sublime Text 4, with a Lua plugin for syntax highlighting. It works pretty well, although it doesn’t understand backtick strings.
I have luau installed on my Mac, via homebrew. I just followed the instructions. I did have to set up a “build system”. It looks like this:
{
"cmd": ["luau", "$file"],
"selector": "source.luau",
"file_regex": "^(...*?):([0-9]*):?([0-9]*)",
"line_regex": "^(?:stdin|(...*?)):([0-9]*):?([0-9]*)",
"working_dir": "$file_path"
}
If I recall, I just took an existing one and substituted “luau” into the “cmd” line.
With that file in place and a luau file open in Sublime, and that file in the active tab, if I type Command+B, the build system will compile and run the file. F7 will also run it. I get my results in a pop-up panel in Sublime. If all goes well, it looks like this:
All Tests: Pass 867 Fail 0. Tests:run_tests
[Finished in 32ms]
Naturally, if the tests encounter any errors, they will be listed there, as will compile errors, in the frequent event that I have a syntax error in the file.
Presently, I have a file test.lua that contains everything I’m working on, including the class
function, the Tests
object, and all my tests and all my code. As of this morning, that file is 783 lines long. It contains essentially everything I’ve worked on so far, including classes Interpolator, Bezier, L_Bezier, D_Bezier, Waypoint, and Multipath, as well as a vector
class that I’ve built to support my work.
With a file that large, work isn’t as smooth as I’d like. I have to keep scrolling between the tests and the code, and it’s hundreds of lines between them. I’ve tried using a 4-panel editor layout and multiple tabs open on the same file, but so far it’s just not convenient.
If I were using a real IDE, like PyCharm or IDEA, I would have a separate file for every class, another file for its tests, and an editor hierarchy open in the left column that would let me quickly select the file I wanted to read or change.
I’d like to get to a similar situation with Luau.
As presently set up, our download above are not what I need for my own work. They are somewhat suitable for inclusion in SLua code and less suitable for my home setup. As part of this effort, I’ll try to set them up to be useful for both situations. Since I have essentially zero users just now, I can probably work freely as I see fit. Incremental improvement, my main life strategy.
Luau has a language feature require
that allows a file to refer to another and bring in that file’s definitions. It’s not an include
like the one in Python. A file used in require
must define a table, and return that table as the result of the require
call.
We can make that work for Tests
right now, except that the current Tests file includes both the framework and the tests that test and demonstrate the framework. When we physically include that file, as we would with SLua, we can just remove those sample tests and replace with our own. So one thing I need to do is split out the sample tests from the Tests object.
The class
function is a bit more of a problem if I want to use require
locally—and I do. Since require
returns a table, I need to wrap class
in a table one way or another.
Enough warmup chatter, let’s do something. I’ll start by splitting tests into a file named tests and a file named tests-samples, and then see if I can require them.
That was quickly done. I now have two additional files in my repo folder, tests.lua
and test-samples.lua
I don’t think that the latter will work yet, as it does not return a table.
Let’s see if we can make that work. I add a require
at the top to provide access to Tests, and then return Tests as the return from that require. When I do this in my main file:
local Tests = require('./tests')
require('./test-samples')
All my existing tests run, plus the samples, some of which fail as intended, to show how the framework works.
So far so good.
I can see two main paths ahead. One will be modifying the class
function so that it can be in a require
file. The other path is to break out the objects I’ve created into fewer files: my ‘test.lua’ working file is still 689 lines after removing the initial Tests class.
I think I’ll get greater benefit by breaking out separate files. The class
function is only about 45 lines and I could make it shorter by removing is_a
, which I never use.
Let’s remove vector
. I am briefly thwarted, because my vector
is a class and therefore needs my class
function. I’ll include it for now. OK, vector is now separate and required in. My 867 tests are still running.
Even though further separation is easier and more effective at reducing my file size, I think I’d better figure out how to make the class
work. Here’s the class function now:
function class(base)
local c = {} -- a new class instance
if type(base) == 'table' then
-- our new class is a shallow copy of the base class!
for i,v in pairs(base) do
c[i] = v
end
c._base = base
end
-- the class will be the metatable for all its objects,
-- and they will look up their methods in it.
c.__index = c
-- expose a constructor which can be called by <classname>( <args> )
local mt = {}
mt.__call = function(class_tbl, ...)
local obj = {}
setmetatable(obj,c)
if class_tbl.init then
class_tbl.init(obj,...)
else
-- make sure that any stuff from the base class is initialized!
if base and base.init then
base.init(obj, ...)
end
end
return obj
end
c.is_a = function(self, klass)
local m = getmetatable(self)
while m do
if m == klass then return true end
m = m._base
end
return false
end
setmetatable(c, mt)
return c
end
We need to rig up a table, and what we would like to have work is this:
class = require('./class')
So we need a table to return, and that table needs to be callable.
I’ll try to do this in line and then move it.
local class = {}
local meta = {}
meta.__index = meta
meta.__call = function(table, base)
return table.class(base)
end
setmetatable(class, meta)
function class.class(base)
...
My tests all run. Try moving this out and requiring it as intended.
Seems to work. Change the vector file to use it. Works. Here’s the top of my test.lua file now:
local class = require('./class')
local Tests = require('./tests')
-- require('./test-samples')
local vector = require('./vector')
It’s still pretty long, 598 lines. Of those, About 300 are tests and 300 are actual code. Not an unreasonable ratio, I guess.
There are classes that I’d like to preserve but get out of sight, like Interpolator or D_Bezier, which are either ideas that didn’t pan out or that have been superseded.
The Bezier class itself is just about 100 lines, and includes a lot of methods that we will probably no longer want, such as compute_length
and estimate_length
and certainly not create_waypoints
, which is now replaced with the L_Bezier’s partitioning.
I think I’ll let this ride for now. The test.lua file is down 183 lines from its maximum, and we have our two key components, class
and Tests
broken out. More will come.
For now, it’s chai time. Safe paths!