From 2b8a0764bcd810f79f1a98f1a222b26d8538b862 Mon Sep 17 00:00:00 2001 From: Robert Jelic <36573031+NoryiE@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:47:00 +0100 Subject: [PATCH] LuaLS Test --- .github/workflows/docs.yml | 8 +- .github/workflows/lualsgen.yml | 36 ++++++ .gitignore | 1 + LuaLS.lua | 203 +++++++++++++++++++++++++++++ annotationParser.lua | 226 +++++++++++++++++++++++++++++++++ example.lua | 31 +++++ markdown.lua | 40 +++++- src/elements/BaseElement.lua | 2 +- src/elements/BaseFrame.lua | 1 + src/elements/Button.lua | 3 + src/elements/Container.lua | 1 + src/elements/Frame.lua | 2 +- src/elements/VisualElement.lua | 48 ++++++- src/main.lua | 2 +- src/propertySystem.lua | 1 + 15 files changed, 589 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/lualsgen.yml create mode 100644 .gitignore create mode 100644 LuaLS.lua create mode 100644 annotationParser.lua create mode 100644 example.lua diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e557bc7..4930836 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -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' }} diff --git a/.github/workflows/lualsgen.yml b/.github/workflows/lualsgen.yml new file mode 100644 index 0000000..40421dc --- /dev/null +++ b/.github/workflows/lualsgen.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecbf039 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +test.lua \ No newline at end of file diff --git a/LuaLS.lua b/LuaLS.lua new file mode 100644 index 0000000..b7ddfac --- /dev/null +++ b/LuaLS.lua @@ -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 diff --git a/annotationParser.lua b/annotationParser.lua new file mode 100644 index 0000000..b063a3b --- /dev/null +++ b/annotationParser.lua @@ -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 ") +end \ No newline at end of file diff --git a/example.lua b/example.lua new file mode 100644 index 0000000..3d422ed --- /dev/null +++ b/example.lua @@ -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) diff --git a/markdown.lua b/markdown.lua index fdff881..2bc6e30 100644 --- a/markdown.lua +++ b/markdown.lua @@ -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 diff --git a/src/elements/BaseElement.lua b/src/elements/BaseElement.lua index 9f3c79f..63a6e1a 100644 --- a/src/elements/BaseElement.lua +++ b/src/elements/BaseElement.lua @@ -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 = {} diff --git a/src/elements/BaseFrame.lua b/src/elements/BaseFrame.lua index eca4476..996816e 100644 --- a/src/elements/BaseFrame.lua +++ b/src/elements/BaseFrame.lua @@ -1,6 +1,7 @@ local Container = require("elements/Container") local Render = require("render") +---@class BaseFrame : Container local BaseFrame = setmetatable({}, Container) BaseFrame.__index = BaseFrame diff --git a/src/elements/Button.lua b/src/elements/Button.lua index 7dfc72a..9a93088 100644 --- a/src/elements/Button.lua +++ b/src/elements/Button.lua @@ -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 diff --git a/src/elements/Container.lua b/src/elements/Container.lua index d158094..155a440 100644 --- a/src/elements/Container.lua +++ b/src/elements/Container.lua @@ -4,6 +4,7 @@ local expect = require("libraries/expect") local max = math.max +---@class Container : VisualElement local Container = setmetatable({}, VisualElement) Container.__index = Container diff --git a/src/elements/Frame.lua b/src/elements/Frame.lua index b86b2cc..2dd7459 100644 --- a/src/elements/Frame.lua +++ b/src/elements/Frame.lua @@ -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) diff --git a/src/elements/VisualElement.lua b/src/elements/VisualElement.lua index fd0fef9..7e7062d 100644 --- a/src/elements/VisualElement.lua +++ b/src/elements/VisualElement.lua @@ -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 diff --git a/src/main.lua b/src/main.lua index a7c52b9..db16f7f 100644 --- a/src/main.lua +++ b/src/main.lua @@ -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) diff --git a/src/propertySystem.lua b/src/propertySystem.lua index eebc0bf..82f1156 100644 --- a/src/propertySystem.lua +++ b/src/propertySystem.lua @@ -1,6 +1,7 @@ local deepCopy = require("libraries/utils").deepCopy local expect = require("libraries/expect") +--- @class PropertySystem local PropertySystem = {} PropertySystem.__index = PropertySystem