LuaLS Test

This commit is contained in:
Robert Jelic
2025-02-10 16:47:00 +01:00
parent d55a80dc0e
commit 2b8a0764bc
15 changed files with 589 additions and 16 deletions

View File

@@ -26,16 +26,16 @@ jobs:
with:
ref: gh-pages
path: build_docs
- name: Prepare content directory
- name: Prepare references directory
run: |
mkdir -p build_docs/docs
rm -rf build_docs/docs/content
mkdir -p build_docs/docs/content
rm -rf build_docs/docs/references
mkdir -p build_docs/docs/references
- name: Process markdown files
run: |
find src -type f -name "*.lua" | while read file; do
filename=$(basename "$file")
lua markdown.lua "$file" "build_docs/docs/content/${filename%.lua}.md"
lua markdown.lua "$file" "build_docs/docs/references/${filename%.lua}.md"
done
- name: Deploy
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}

36
.github/workflows/lualsgen.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Generate LuaLS Definitions
on:
push:
paths:
- 'src/elements/**'
branches:
- main
pull_request:
paths:
- 'src/elements/**'
branches:
- main
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Lua
uses: leafo/gh-actions-lua@v8
with:
luaVersion: "5.4"
- name: Generate LuaLS definitions
run: |
lua annotationParser.lua src/elements src/LuaLS.lua
- name: Commit changes
if: github.event_name == 'push'
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add src/LuaLS.lua
git commit -m "Update LuaLS definitions" || true
git push

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
test.lua

203
LuaLS.lua Normal file
View File

@@ -0,0 +1,203 @@
---@class Button
local Button = {}
--- The event that is triggered when the button is clicked
---@generic Element: Button
---@param self Element
---@param callback function
---@return Element
function Button:onMouseClick(callback)
return self
end
---@class Container
local Container = {}
--- Adds a new Button to the container
---@generic Element: Container
---@param self Element
---@return Button
function Container:addButton()
return self
end
--- Adds a new Container to the container
---@generic Element: Container
---@param self Element
---@return Container
function Container:addContainer()
return self
end
--- Adds a new Frame to the container
---@generic Element: Container
---@param self Element
---@return Frame
function Container:addFrame()
return self
end
--- Adds a new VisualElement to the container
---@generic Element: Container
---@param self Element
---@return VisualElement
function Container:addVisualElement()
return self
end
---@class VisualElement
---@field x number
---@field y number
---@field z number
---@field width number
---@field height number
---@field background color
---@field foreground color
---@field clicked boolean
local VisualElement = {}
--- Gets the x position of the element
---@generic Element: VisualElement
---@param self Element
---@return number
function VisualElement:getX()
return self.x
end
--- Sets the x position of the element
---@generic Element: VisualElement
---@param self Element
---@param x number
---@return Element
function VisualElement:setX(x)
self.x = x
return self
end
--- Gets the y position of the element
---@generic Element: VisualElement
---@param self Element
---@return number
function VisualElement:getY()
return self.y
end
--- Sets the y position of the element
---@generic Element: VisualElement
---@param self Element
---@param y number
---@return Element
function VisualElement:setY(y)
self.y = y
return self
end
--- Gets the z position of the element
---@generic Element: VisualElement
---@param self Element
---@return number
function VisualElement:getZ()
return self.z
end
--- Sets the z position of the element
---@generic Element: VisualElement
---@param self Element
---@param z number
---@return Element
function VisualElement:setZ(z)
self.z = z
return self
end
--- Gets the width of the element
---@generic Element: VisualElement
---@param self Element
---@return number
function VisualElement:getWidth()
return self.width
end
--- Sets the width of the element
---@generic Element: VisualElement
---@param self Element
---@param width number
---@return Element
function VisualElement:setWidth(width)
self.width = width
return self
end
--- Gets the height of the element
---@generic Element: VisualElement
---@param self Element
---@return number
function VisualElement:getHeight()
return self.height
end
--- Sets the height of the element
---@generic Element: VisualElement
---@param self Element
---@param height number
---@return Element
function VisualElement:setHeight(height)
self.height = height
return self
end
--- Gets the background color of the element
---@generic Element: VisualElement
---@param self Element
---@return color
function VisualElement:getBackground()
return self.background
end
--- Sets the background color of the element
---@generic Element: VisualElement
---@param self Element
---@param background color
---@return Element
function VisualElement:setBackground(background)
self.background = background
return self
end
--- Gets the foreground color of the element
---@generic Element: VisualElement
---@param self Element
---@return color
function VisualElement:getForeground()
return self.foreground
end
--- Sets the foreground color of the element
---@generic Element: VisualElement
---@param self Element
---@param foreground color
---@return Element
function VisualElement:setForeground(foreground)
self.foreground = foreground
return self
end
--- Gets the element is currently clicked
---@generic Element: VisualElement
---@param self Element
---@return boolean
function VisualElement:getClicked()
return self.clicked
end
--- Sets the element is currently clicked
---@generic Element: VisualElement
---@param self Element
---@param clicked boolean
---@return Element
function VisualElement:setClicked(clicked)
self.clicked = clicked
return self
end

226
annotationParser.lua Normal file
View File

@@ -0,0 +1,226 @@
local function parseProperty(line)
-- Matches: ---@property name type default description
local name, type, default, description = line:match("%-%-%-@property%s+(%w+)%s+(%w+)%s+(.-)%s+(.*)")
if name and type then
-- Generate field annotation
local fieldDef = string.format("---@field %s %s\n", name, type)
-- Generate getter annotation and function
local getterDoc = string.format([[
--- Gets the %s
---@generic T: %s
---@param self T
---@return %s
]], description, "VisualElement", type)
local getterFunc = string.format([[
function VisualElement:get%s()
return self.%s
end
]], name:sub(1,1):upper() .. name:sub(2), name)
-- Generate setter annotation and function
local setterDoc = string.format([[
--- Sets the %s
---@generic T: %s
---@param self T
---@param %s %s
---@return T
]], description, "VisualElement", name, type)
local setterFunc = string.format([[
function VisualElement:set%s(%s)
self.%s = %s
return self
end
]], name:sub(1,1):upper() .. name:sub(2), name, name, name)
return fieldDef .. getterDoc .. getterFunc .. setterDoc .. setterFunc
end
end
local input = [[
---@property x number 1 The x position of the element
---@property y number 1 The y position of the element
]]
for line in input:gmatch("[^\r\n]+") do
parseProperty(line)
end
local function findClassName(content)
return content:match("%-%-%-@class%s+(%w+)")
end
local function parseProperties(content)
local properties = {}
for line in content:gmatch("[^\r\n]+") do
local name, type, default, desc = line:match("%-%-%-@property%s+(%w+)%s+(%w+)%s+(.-)%s+(.*)")
if name and type then
properties[#properties + 1] = {
name = name,
type = type,
default = default,
description = desc
}
end
end
return properties
end
local function parseEvents(content)
local events = {}
for line in content:gmatch("[^\r\n]+") do
local name, description = line:match("%-%-%-@event%s+([%w_]+)%s+(.+)")
if name then
local functionName = name:gsub("_(%w)", function(c) return c:upper() end)
functionName = "on" .. functionName:sub(1,1):upper() .. functionName:sub(2)
events[#events + 1] = {
name = name,
functionName = functionName,
description = description
}
end
end
return events
end
local function collectAllClassNames(folder)
local classes = {}
local files = fs.list(folder)
for _, filename in ipairs(files) do
if filename:match("%.lua$") then
local path = fs.combine(folder, filename)
local f = fs.open(path, "r")
if f then
local content = f.readAll()
f.close()
local className = findClassName(content)
if className and className ~= "BaseFrame" then
table.insert(classes, className)
end
end
end
end
return classes
end
local function generateClassContent(className, properties, events, allClasses)
if #properties == 0 and #events == 0 and className ~= "Container" then
return nil
end
local content = {}
table.insert(content, string.format("---@class %s", className))
for _, prop in ipairs(properties) do
table.insert(content, string.format("---@field %s %s", prop.name, prop.type))
end
table.insert(content, string.format("local %s = {}", className))
table.insert(content, "")
for _, prop in ipairs(properties) do
table.insert(content, string.format("--- Gets the %s", prop.description))
table.insert(content, string.format("---@generic Element: %s", className))
table.insert(content, "---@param self Element")
table.insert(content, string.format("---@return %s", prop.type))
table.insert(content, string.format("function %s:get%s()",
className,
prop.name:sub(1,1):upper() .. prop.name:sub(2)
))
table.insert(content, string.format(" return self.%s", prop.name))
table.insert(content, "end")
table.insert(content, "")
table.insert(content, string.format("--- Sets the %s", prop.description))
table.insert(content, string.format("---@generic Element: %s", className))
table.insert(content, "---@param self Element")
table.insert(content, string.format("---@param %s %s", prop.name, prop.type))
table.insert(content, "---@return Element")
table.insert(content, string.format("function %s:set%s(%s)",
className,
prop.name:sub(1,1):upper() .. prop.name:sub(2),
prop.name
))
table.insert(content, string.format(" self.%s = %s", prop.name, prop.name))
table.insert(content, " return self")
table.insert(content, "end")
table.insert(content, "")
end
for _, event in ipairs(events) do
table.insert(content, string.format([[
--- %s
---@generic Element: %s
---@param self Element
---@param callback function
---@return Element
function %s:%s(callback)
return self
end]], event.description, className, className, event.functionName))
table.insert(content, "")
end
if className == "Container" then
for _, cls in ipairs(allClasses) do
table.insert(content, string.format([[
--- Adds a new %s to the container
---@generic Element: Container
---@param self Element
---@return %s
function Container:add%s()
return self
end]], cls, cls, cls))
table.insert(content, "")
end
end
return table.concat(content, "\n")
end
local function parseFolder(folder, destinationFile)
local allClasses = collectAllClassNames(folder)
local allContent = {}
local files = fs.list(folder)
for _, filename in ipairs(files) do
if filename:match("%.lua$") then
local path = fs.combine(folder, filename)
local f = fs.open(path, "r")
if f then
local content = f.readAll()
f.close()
local className = findClassName(content)
if className then
local properties = parseProperties(content)
local events = parseEvents(content)
local classContent = generateClassContent(className, properties, events, allClasses)
if classContent then -- Only add if content was generated
table.insert(allContent, classContent)
end
end
end
end
end
local f = fs.open(destinationFile, "w")
if f then
f.write(table.concat(allContent, "\n\n"))
f.close()
end
end
local args = {...}
if #args == 2 then
parseFolder(args[1], args[2])
else
print("Usage: annotationParser <sourceFolder> <destinationFile>")
end

31
example.lua Normal file
View File

@@ -0,0 +1,31 @@
---@generic Element: Animal
---@class Animal
---@field setName fun(self: Element, name: string): Element
local Animal = {}
---@class Dog : Animal
---@field setSpeed fun(self: Dog, speed: number): Dog
local Dog = setmetatable({}, { __index = Animal })
---@generic Element: Animal
---@param self Element
---@param length string
---@return Element
function Animal:setLength(length)
self.length = length
return self
end
function Dog:setSpeed(speed)
self.speed = speed
return self
end
---@return Dog
function Dog.new()
return setmetatable({}, { __index = Dog })
end
local dog = Dog.new()
dog:setName("Rex")
:setSpeed(10)

View File

@@ -4,6 +4,7 @@ local markdown = {
local commentTypes = {
"module",
"class",
"param",
"return",
"usage",
@@ -79,7 +80,11 @@ function markdown.parse(content)
if(commentType == "module")then
currentBlock.usageIsActive = false
currentBlock.type = "module"
currentBlock.moduleName = value
currentBlock.moduleName = value:match("([^%s]+)")
elseif(commentType == "class")then
currentBlock.usageIsActive = false
currentBlock.type = "class"
currentBlock.className = value
end
if(commentType == "usage")then
currentBlock.usage = currentBlock.usage or {}
@@ -205,7 +210,7 @@ local function markdownEvents()
end
local output = "\n## Events\n\n"
for _, block in pairs(markdown.blocks) do
if block.type == "module" then
if block.type == "module" or block.type == "class" then
if(block.event~=nil)then
for _, line in pairs(block.event) do
output = output .. "* " .. line .. "\n"
@@ -216,7 +221,7 @@ local function markdownEvents()
return output
end
local function markdownModuleFunctions()
local function markdownModuleOrClassFunctions()
if(#markdown.blocks<=0)then
return ""
end
@@ -260,7 +265,32 @@ local function markdownModule(block)
output = output .. markdownProperties()
output = output .. markdownEvents()
output = output .. markdownModuleFunctions(block)
output = output .. markdownModuleOrClassFunctions(block)
output = output .. "\n"
return output
end
local function markdownClass(block)
local output = "# ".. block.className.."\n"
if(block.usage~=nil)then
if(#block.usage > 0)then
for k,v in pairs(block.usage) do
local _output = "\n### Usage\n ```lua\n"
for _, line in pairs(v.content) do
_output = _output .. line .. "\n"
end
_output = _output .. "```\n"
table.insert(block.desc, v.line, _output)
end
end
end
for _, line in pairs(block.desc) do
output = output .. line .. "\n"
end
output = output .. markdownProperties()
output = output .. markdownEvents()
output = output .. markdownModuleOrClassFunctions(block)
output = output .. "\n"
return output
end
@@ -276,6 +306,8 @@ function markdown.makeMarkdown()
end
elseif block.type == "module" then
output = output .. markdownModule(block)
elseif block.type == "class" then
output = output .. markdownClass(block)
end
end

View File

@@ -1,7 +1,7 @@
local PropertySystem = require("propertySystem")
--- The base class for all UI elements in Basalt
-- @module BaseElement
--- @class BaseElement : PropertySystem
local BaseElement = setmetatable({}, PropertySystem)
BaseElement.__index = BaseElement
BaseElement._events = {}

View File

@@ -1,6 +1,7 @@
local Container = require("elements/Container")
local Render = require("render")
---@class BaseFrame : Container
local BaseFrame = setmetatable({}, Container)
BaseFrame.__index = BaseFrame

View File

@@ -1,10 +1,13 @@
local VisualElement = require("elements/VisualElement")
local getCenteredPosition = require("libraries/utils").getCenteredPosition
---@class Button : VisualElement
local Button = setmetatable({}, VisualElement)
Button.__index = Button
Button.defineProperty(Button, "text", {default = "Button", type = "string"})
---@event mouse_click The event that is triggered when the button is clicked
Button.listenTo(Button, "mouse_click")
---@diagnostic disable-next-line: duplicate-set-field

View File

@@ -4,6 +4,7 @@ local expect = require("libraries/expect")
local max = math.max
---@class Container : VisualElement
local Container = setmetatable({}, VisualElement)
Container.__index = Container

View File

@@ -1,9 +1,9 @@
local Container = require("elements/Container")
---@class Frame : Container
local Frame = setmetatable({}, Container)
Frame.__index = Frame
---@diagnostic disable-next-line: duplicate-set-field
function Frame.new(id, basalt)
local self = setmetatable({}, Frame):__init()
self:init(id, basalt)

View File

@@ -1,10 +1,17 @@
local BaseElement = require("elements/BaseElement")
---@alias color number
---@class VisualElement : BaseElement
local VisualElement = setmetatable({}, BaseElement)
VisualElement.__index = VisualElement
local tHex = require("libraries/colorHex")
---@property x number 1 x position of the element
BaseElement.defineProperty(VisualElement, "x", {default = 1, type = "number", canTriggerRender = true})
---@property y number 1 y position of the element
BaseElement.defineProperty(VisualElement, "y", {default = 1, type = "number", canTriggerRender = true})
---@property z number 1 z position of the element
BaseElement.defineProperty(VisualElement, "z", {default = 1, type = "number", canTriggerRender = true, setter = function(self, value)
self.basalt.LOGGER.debug("Setting z to " .. value)
if self.parent then
@@ -12,13 +19,22 @@ BaseElement.defineProperty(VisualElement, "z", {default = 1, type = "number", ca
end
return value
end})
---@property width number 1 width of the element
BaseElement.defineProperty(VisualElement, "width", {default = 1, type = "number", canTriggerRender = true})
---@property height number 1 height of the element
BaseElement.defineProperty(VisualElement, "height", {default = 1, type = "number", canTriggerRender = true})
---@property background color black background color of the element
BaseElement.defineProperty(VisualElement, "background", {default = colors.black, type = "number", canTriggerRender = true})
---@property foreground color white foreground color of the element
BaseElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true})
---@property clicked boolean false element is currently clicked
BaseElement.defineProperty(VisualElement, "clicked", {default = false, type = "boolean"})
---@diagnostic disable-next-line: duplicate-set-field
--- Creates a new VisualElement instance
--- @param id string The unique identifier for this element
--- @param basalt table The basalt instance
--- @return VisualElement object The newly created VisualElement instance
--- @usage local element = VisualElement.new("myId", basalt)
function VisualElement.new(id, basalt)
local self = setmetatable({}, VisualElement):__init()
self:init(id, basalt)
@@ -26,18 +42,35 @@ function VisualElement.new(id, basalt)
return self
end
--- Draws a text character/fg/bg at the specified position with a certain size, used in the rendering system
--- @param x number The x position to draw
--- @param y number The y position to draw
--- @param width number The width of the element
--- @param height number The height of the element
--- @param text string The text char to draw
--- @param fg color The foreground color
--- @param bg color The background color
function VisualElement:multiBlit(x, y, width, height, text, fg, bg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
self.parent:multiBlit(x, y, width, height, text, fg, bg)
end
--- Draws a text character at the specified position, used in the rendering system
--- @param x number The x position to draw
--- @param y number The y position to draw
--- @param text string The text char to draw
--- @param fg color The foreground color
function VisualElement:textFg(x, y, text, fg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
self.parent:textFg(x, y, text, fg)
end
--- Checks if the specified coordinates are within the bounds of the element
--- @param x number The x position to check
--- @param y number The y position to check
--- @return boolean isInBounds Whether the coordinates are within the bounds of the element
function VisualElement:isInBounds(x, y)
local xPos, yPos = self.get("x"), self.get("y")
local width, height = self.get("width"), self.get("height")
@@ -46,6 +79,11 @@ function VisualElement:isInBounds(x, y)
y >= yPos and y <= yPos + height - 1
end
--- Handles a mouse click event
--- @param button number The button that was clicked
--- @param x number The x position of the click
--- @param y number The y position of the click
--- @return boolean clicked Whether the element was clicked
function VisualElement:mouse_click(button, x, y)
if self:isInBounds(x, y) then
self.set("clicked", true)
@@ -74,8 +112,8 @@ function VisualElement:handleEvent(event, ...)
end
--- Returns the absolute position of the element or the given coordinates.
---@param x? number -- x position
---@param y? number -- y position
---@param x? number x position
---@param y? number y position
function VisualElement:getAbsolutePosition(x, y)
if (x == nil) or (y == nil) then
x, y = self.get("x"), self.get("y")
@@ -93,8 +131,8 @@ function VisualElement:getAbsolutePosition(x, y)
end
--- Returns the relative position of the element or the given coordinates.
---@param x? number -- x position
---@param y? number -- y position
---@param x? number x position
---@param y? number y position
---@return number, number
function VisualElement:getRelativePosition(x, y)
if (x == nil) or (y == nil) then

View File

@@ -133,7 +133,7 @@ function basalt.stop()
end
--- Starts the Basalt runtime
--- @param isActive boolean Whether to start active (default: true)
--- @param isActive? boolean Whether to start active (default: true)
--- @usage basalt.run()
--- @usage basalt.run(false)
function basalt.run(isActive)

View File

@@ -1,6 +1,7 @@
local deepCopy = require("libraries/utils").deepCopy
local expect = require("libraries/expect")
--- @class PropertySystem
local PropertySystem = {}
PropertySystem.__index = PropertySystem