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/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 5b74969d..2e47094d 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -1,62 +1,95 @@ -local PATH = (...):gsub('%.init$', '') +-- 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") + +--[[ +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 + +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 +]] + 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"), - ---@see ChangeInputButton - ---@type fun(options: ChangeInputButtonOptions): ChangeInputButton - ChangeInputButton = require(PATH .. ".ChangeInputButton"), - 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"), - KeyBindingMenuItem = require(PATH .. ".KeyBindingMenuItem"), - ---@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"), - SliderMenuItem = require(PATH .. ".SliderMenuItem"), + ---@source BoolSelector.lua + BoolSelector = import("./BoolSelector"), + ---@source Button.lua + Button = import("./Button"), + ---@source ButtonGroup.lua + ButtonGroup = import("./ButtonGroup"), + ---@source Carousel.lua + Carousel = import("./Carousel"), + ---@source ChangeInputButton.lua + ChangeInputButton = import("./ChangeInputButton"), + ---@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"), + ---@source ImageContainer.lua + ImageContainer = import("./ImageContainer"), + ---@source InputField.lua + InputField = import("./InputField"), + ---@source KeyBindingMenuItem.lua + KeyBindingMenuItem = import("./KeyBindingMenuItem"), + ---@source Label.lua + Label = import("./Label"), + ---@source Leaderboard.lua + Leaderboard = import("./Leaderboard"), + ---@source LevelSlider.lua + 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 + PixelFontLabel = import("./PixelFontLabel"), + ---@source ScrollContainer.lua + ScrollContainer = import("./ScrollContainer"), + ---@source ScrollText.lua + ScrollText = import("./ScrollText"), + ---@source Slider.lua + Slider = import("./Slider"), + ---@source SliderMenuItem.lua + SliderMenuItem = import("./SliderMenuItem"), ---@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"), + ---@source StackPanel.lua + StackPanel = import("./StackPanel"), + ---@source StageCarousel.lua + StageCarousel = import("./StageCarousel"), + ---@source Stepper.lua + Stepper = import("./Stepper"), + ---@source TextButton.lua + TextButton = import("./TextButton"), + ---@source UiElement.lua + UiElement = import("./UIElement"), + ---@source ValueLabel.lua + 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