From 45e50018b7805a7259180e4a711b78c9459c3541 Mon Sep 17 00:00:00 2001 From: Endaris Date: Fri, 16 Jan 2026 22:15:46 +0100 Subject: [PATCH 1/3] add an import function and a supporting plugin for the language server to facilitate easier navigation in the UI "namespace" --- .luarc.json | 9 ++- client/src/ui/init.lua | 115 +++++++++++++++++++++---------------- common/lib/import.lua | 68 ++++++++++++++++++++++ common/lib/luaLsPlugin.lua | 83 ++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 52 deletions(-) create mode 100644 common/lib/import.lua create mode 100644 common/lib/luaLsPlugin.lua diff --git a/.luarc.json b/.luarc.json index 85096c08..bcf6b2b7 100644 --- a/.luarc.json +++ b/.luarc.json @@ -48,6 +48,11 @@ "hint.semicolon": "Disable", + "runtime.plugin": "common/lib/luaLsPlugin.lua", + + // to support mixin union types where a table is both types rather than just one + "type.weakUnionCheck": true, + "workspace.checkThirdParty": false, "workspace.ignoreDir": [ "client/lib/rich_presence/", @@ -61,7 +66,9 @@ "common/lib/socket.lua", "common/lib/dkjson.lua", "common/lib/csprng.lua", - "common/lib/jprof/MessagePack.lua" + "common/lib/jprof/MessagePack.lua", + "common/lib/import.lua", + "common/lib/luaLsPlugin.lua" ], "workspace.library": [ ".vscode/love2d-12/library" diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index a884de6b..c2ea5841 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -1,57 +1,70 @@ -local PATH = (...):gsub('%.init$', '') +local import = require("common.lib.import") + +--[[ +tag each with +---@source relative path +that way "Go to source" on an import of ui elsewhere will lead to the respective source instead of this file +the "./" is assumed given for relative paths but it's still a path so adding the file extension is necessary +when addressing files in subdirectories of ui use forward slashes as the path separator +https://luals.github.io/wiki/annotations/#source +]] + local ui = { - ---@see BoolSelector - ---@type fun(options: BoolSelectorOptions): BoolSelector - BoolSelector = require(PATH .. ".BoolSelector"), - ---@see Button - ---@type fun(options: ButtonOptions): Button - Button = require(PATH .. ".Button"), - ButtonGroup = require(PATH .. ".ButtonGroup"), - Carousel = require(PATH .. ".Carousel"), - Focusable = require(PATH .. ".Focusable"), - FocusDirector = require(PATH .. ".FocusDirector"), - Grid = require(PATH .. ".Grid"), - GridCursor = require(PATH .. ".GridCursor"), - ---@see ImageButton - ---@type fun(options: ImageButtonOptions): ImageButton - ImageButton = require(PATH .. ".ImageButton"), - ImageContainer = require(PATH .. ".ImageContainer"), - InputField = require(PATH .. ".InputField"), - ---@see Label - ---@type fun(options: LabelOptions): Label - Label = require(PATH .. ".Label"), - Leaderboard = require(PATH .. ".Leaderboard"), - ---@see LevelSlider - ---@type fun(options: SliderOptions): LevelSlider - LevelSlider = require(PATH .. ".LevelSlider"), - Menu = require(PATH .. ".Menu"), - MenuItem = require(PATH .. ".MenuItem"), - MultiPlayerSelectionWrapper = require(PATH .. ".MultiPlayerSelectionWrapper"), - PagedUniGrid = require(PATH .. ".PagedUniGrid"), - PanelCarousel = require(PATH .. ".PanelCarousel"), - ---@see PixelFontLabel - ---@type fun(options: PixelFontLabelOptions): PixelFontLabel - PixelFontLabel = require(PATH .. ".PixelFontLabel"), - ---@see ScrollContainer - ---@type fun(options: ScrollContainerOptions): ScrollContainer - ScrollContainer = require(PATH .. ".ScrollContainer"), - ScrollText = require(PATH .. ".ScrollText"), - ---@see Slider - ---@type fun(options: SliderOptions): Slider - Slider = require(PATH .. ".Slider"), + ---@source BoolSelector.lua + BoolSelector = import("./BoolSelector"), + ---@source Button.lua + Button = import("./Button"), + ---@source ButtonGroup.lua + ButtonGroup = import("./ButtonGroup"), + Carousel = import("./Carousel"), + Focusable = import("./Focusable"), + FocusDirector = import("./FocusDirector"), + Grid = import("./Grid"), + GridCursor = import("./GridCursor"), + ---@source ImageButton.lua + ImageButton = import("./ImageButton"), + ---@source ImageContainer.lua + ImageContainer = import("./ImageContainer"), + ---@source InputField.lua + InputField = import("./InputField"), + ---@source Label.lua + ---@type Label + Label = import("./Label"), + Leaderboard = import("./Leaderboard"), + ---@source LevelSlider.lua + ---@type LevelSlider + LevelSlider = import("./LevelSlider"), + ---@source Menu.lua + Menu = import("./Menu"), + ---@source MenuItem.lua + MenuItem = import("./MenuItem"), + MultiPlayerSelectionWrapper = import("./MultiPlayerSelectionWrapper"), + PagedUniGrid = import("./PagedUniGrid"), + ---@source PanelCarousel.lua + PanelCarousel = import("./PanelCarousel"), + ---@source PixelFontLabel.lua + ---@type PixelFontLabel + PixelFontLabel = import("./PixelFontLabel"), + ---@source ScrollContainer.lua + ---@type ScrollContainer + ScrollContainer = import("./ScrollContainer"), + ScrollText = import("./ScrollText"), + ---@source Slider.lua + ---@type Slider + Slider = import("./Slider"), ---@source StackElement.lua - StackElement = require(PATH .. ".StackElement"), - StackPanel = require(PATH .. ".StackPanel"), - StageCarousel = require(PATH .. ".StageCarousel"), - Stepper = require(PATH .. ".Stepper"), - ---@see TextButton - ---@type fun(options: TextButtonOptions): TextButton - TextButton = require(PATH .. ".TextButton"), - ---@see UiElement - ---@type fun(options:UiElementOptions): UiElement - UiElement = require(PATH .. ".UIElement"), - ValueLabel = require(PATH .. ".ValueLabel"), + StackElement = import("./StackElement"), + StackPanel = import("./StackPanel"), + StageCarousel = import("./StageCarousel"), + Stepper = import("./Stepper"), + ---@source TextButton.lua + ---@type TextButton + TextButton = import("./TextButton"), + ---@source UiElement.lua + ---@type UiElement + UiElement = import("./UIElement"), + ValueLabel = import("./ValueLabel"), } return ui \ No newline at end of file diff --git a/common/lib/import.lua b/common/lib/import.lua new file mode 100644 index 00000000..4f2e0de2 --- /dev/null +++ b/common/lib/import.lua @@ -0,0 +1,68 @@ +--[[ +A library to provide a relative require. +Makes sense to use wherever we have grouped files that assuredly only ever move together. +Otherwise require is probably still better. + +MIT License + +Copyright (c) 2023 Justin van der Leij + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + +local function extractPathComponents(path) + local components = {} + for component in path:gmatch("[^/]+") do + table.insert(components, component) + end + + return components +end + +local import = function(path) + local callerPath = debug.getinfo(2, "S").source:sub(2) + + local pathStack = {} + + if (path:sub(1, 1) == ".") then + local components = extractPathComponents(callerPath) + + for i = 1, #components - 1 do + pathStack[i] = components[i] + end + end + + local components = extractPathComponents(path) + + for _, component in ipairs(components) do + if (component == ".") then + -- Skip + elseif (component == "..") then + table.remove(pathStack, #pathStack) + else + table.insert(pathStack, component) + end + end + + local out = table.concat(pathStack, ".") + + return require(out) +end + +return import \ No newline at end of file diff --git a/common/lib/luaLsPlugin.lua b/common/lib/luaLsPlugin.lua new file mode 100644 index 00000000..f98d7143 --- /dev/null +++ b/common/lib/luaLsPlugin.lua @@ -0,0 +1,83 @@ +--[[ +A plugin for the language server to resolve the import libraries requires +Intellisense becomes available for the returned types despite them technically being just a lua function call + +MIT License + +Copyright (c) 2024 Elmārs Āboliņš, including code from Justin van der Leij + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + +local workspace = require "workspace" + +local function extractPathComponents(path) + local components = {} + for component in path:gmatch("[^/]+") do + table.insert(components, component) + end + + return components +end + +local import = function(path, fileUri) + local callerPath = fileUri + + local pathStack = {} + + if (path:sub(1, 1) == ".") then + local components = extractPathComponents(callerPath) + + for i = 1, #components - 1 do + pathStack[i] = components[i] + end + end + + local components = extractPathComponents(path) + + for _, component in ipairs(components) do + if (component == ".") then + -- Skip + elseif (component == "..") then + table.remove(pathStack, #pathStack) + else + table.insert(pathStack, component) + end + end + + local out = table.concat(pathStack, ".") + + return "require(\""..out.."\")" +end + +function OnSetText(uri, text) + local diffs = {} + + local transformedUri = uri:sub(#workspace.rootUri+2) + + for startPos, path, finish in text:gmatch '()import%(([^%(%)]+)%)()' do + diffs[#diffs+1] = { + start = startPos, + finish = finish - 1, + text = import(path:gsub('\"',""), transformedUri), + } + end + + return diffs +end \ No newline at end of file From ab78fc117cb2231a64915c1c669a4186575be09e Mon Sep 17 00:00:00 2001 From: Endaris Date: Fri, 16 Jan 2026 22:50:26 +0100 Subject: [PATCH 2/3] adjust annotations to have ---@source for all currently in ui add some comments what to annotate --- client/src/ui/BoolSelector.lua | 1 + client/src/ui/init.lua | 27 ++++++++++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/client/src/ui/BoolSelector.lua b/client/src/ui/BoolSelector.lua index 2827daeb..7f578ba7 100644 --- a/client/src/ui/BoolSelector.lua +++ b/client/src/ui/BoolSelector.lua @@ -10,6 +10,7 @@ local DebugSettings = require("client.src.debug.DebugSettings") --- A BoolSelector is a UIElement that shows if a setting is on or off and lets you toggle it. ---@class BoolSelector : UiElement +---@operator call(BoolSelectorOptions): BoolSelector ---@field value boolean ---@field vertical boolean ---@field circleRadius number diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index c2ea5841..81bfb287 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -1,3 +1,6 @@ +-- import is getting "live replaced" for intellisense via the lua LS plugin so the editor incorrectly detects it as unused-local +-- but without lua LS it is a real function that manages the relative require +---@diagnostic disable-next-line: unused-local local import = require("common.lib.import") --[[ @@ -7,6 +10,10 @@ that way "Go to source" on an import of ui elsewhere will lead to the respective the "./" is assumed given for relative paths but it's still a path so adding the file extension is necessary when addressing files in subdirectories of ui use forward slashes as the path separator https://luals.github.io/wiki/annotations/#source + +Intellisense for constructors that have their constructor annotated usually works fine if you type +ui.UiElement({}) +and then navigate back into the {} and hit Ctrl+Space for suggestions ]] @@ -17,10 +24,15 @@ local ui = { Button = import("./Button"), ---@source ButtonGroup.lua ButtonGroup = import("./ButtonGroup"), + ---@source Carousel.lua Carousel = import("./Carousel"), + ---@source Focusable.lua Focusable = import("./Focusable"), + ---@source FocusDirector.lua FocusDirector = import("./FocusDirector"), + ---@source Grid.lua Grid = import("./Grid"), + ---@source GridCursor.lua GridCursor = import("./GridCursor"), ---@source ImageButton.lua ImageButton = import("./ImageButton"), @@ -29,41 +41,42 @@ local ui = { ---@source InputField.lua InputField = import("./InputField"), ---@source Label.lua - ---@type Label Label = import("./Label"), + ---@source Leaderboard.lua Leaderboard = import("./Leaderboard"), ---@source LevelSlider.lua - ---@type LevelSlider LevelSlider = import("./LevelSlider"), ---@source Menu.lua Menu = import("./Menu"), ---@source MenuItem.lua MenuItem = import("./MenuItem"), + ---@source MultiPlayerSelectionWrapper.lua MultiPlayerSelectionWrapper = import("./MultiPlayerSelectionWrapper"), + ---@source PagedUniGrid.lua PagedUniGrid = import("./PagedUniGrid"), ---@source PanelCarousel.lua PanelCarousel = import("./PanelCarousel"), ---@source PixelFontLabel.lua - ---@type PixelFontLabel PixelFontLabel = import("./PixelFontLabel"), ---@source ScrollContainer.lua - ---@type ScrollContainer ScrollContainer = import("./ScrollContainer"), + ---@source ScrollText.lua ScrollText = import("./ScrollText"), ---@source Slider.lua - ---@type Slider Slider = import("./Slider"), ---@source StackElement.lua StackElement = import("./StackElement"), + ---@source StackPanel.lua StackPanel = import("./StackPanel"), + ---@source StageCarousel.lua StageCarousel = import("./StageCarousel"), + ---@source Stepper.lua Stepper = import("./Stepper"), ---@source TextButton.lua - ---@type TextButton TextButton = import("./TextButton"), ---@source UiElement.lua - ---@type UiElement UiElement = import("./UIElement"), + ---@source ValueLabel.lua ValueLabel = import("./ValueLabel"), } From 89594e495d1376a7617c0c23808b9ceb10e2f204 Mon Sep 17 00:00:00 2001 From: Endaris Date: Fri, 16 Jan 2026 23:08:31 +0100 Subject: [PATCH 3/3] add extra comment to init.lua --- client/src/ui/init.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 81bfb287..7e5efaa4 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -14,6 +14,12 @@ https://luals.github.io/wiki/annotations/#source Intellisense for constructors that have their constructor annotated usually works fine if you type ui.UiElement({}) and then navigate back into the {} and hit Ctrl+Space for suggestions + +"Go to source" on functions will work after annotating either +---@operator call(argType): classname +or +---@overload fun(options: argType): classname +on the class itself as luaLS only then correctly infers the return from the constructor ]]