Skip to content

Commit e9ba361

Browse files
authored
Quality of Life Improvements for UI namespace (#738)
* add an import function and a supporting plugin for the language server to facilitate easier navigation in the UI "namespace" * adjust annotations to have ---@source for all currently in ui add some comments what to annotate * add extra comment to init.lua
1 parent 9b0e82f commit e9ba361

5 files changed

Lines changed: 249 additions & 57 deletions

File tree

.luarc.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848

4949
"hint.semicolon": "Disable",
5050

51+
"runtime.plugin": "common/lib/luaLsPlugin.lua",
52+
53+
// to support mixin union types where a table is both types rather than just one
54+
"type.weakUnionCheck": true,
55+
5156
"workspace.checkThirdParty": false,
5257
"workspace.ignoreDir": [
5358
"client/lib/rich_presence/",
@@ -61,7 +66,9 @@
6166
"common/lib/socket.lua",
6267
"common/lib/dkjson.lua",
6368
"common/lib/csprng.lua",
64-
"common/lib/jprof/MessagePack.lua"
69+
"common/lib/jprof/MessagePack.lua",
70+
"common/lib/import.lua",
71+
"common/lib/luaLsPlugin.lua"
6572
],
6673
"workspace.library": [
6774
".vscode/love2d-12/library"

client/src/ui/BoolSelector.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ local DebugSettings = require("client.src.debug.DebugSettings")
1010

1111
--- A BoolSelector is a UIElement that shows if a setting is on or off and lets you toggle it.
1212
---@class BoolSelector : UiElement
13+
---@operator call(BoolSelectorOptions): BoolSelector
1314
---@field value boolean
1415
---@field vertical boolean
1516
---@field circleRadius number

client/src/ui/init.lua

Lines changed: 89 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,95 @@
1-
local PATH = (...):gsub('%.init$', '')
1+
-- import is getting "live replaced" for intellisense via the lua LS plugin so the editor incorrectly detects it as unused-local
2+
-- but without lua LS it is a real function that manages the relative require
3+
---@diagnostic disable-next-line: unused-local
4+
local import = require("common.lib.import")
5+
6+
--[[
7+
tag each with
8+
---@source relative path
9+
that way "Go to source" on an import of ui elsewhere will lead to the respective source instead of this file
10+
the "./" is assumed given for relative paths but it's still a path so adding the file extension is necessary
11+
when addressing files in subdirectories of ui use forward slashes as the path separator
12+
https://luals.github.io/wiki/annotations/#source
13+
14+
Intellisense for constructors that have their constructor annotated usually works fine if you type
15+
ui.UiElement({})
16+
and then navigate back into the {} and hit Ctrl+Space for suggestions
17+
18+
"Go to source" on functions will work after annotating either
19+
---@operator call(argType): classname
20+
or
21+
---@overload fun(options: argType): classname
22+
on the class itself as luaLS only then correctly infers the return from the constructor
23+
]]
24+
225

326
local ui = {
4-
---@see BoolSelector
5-
---@type fun(options: BoolSelectorOptions): BoolSelector
6-
BoolSelector = require(PATH .. ".BoolSelector"),
7-
---@see Button
8-
---@type fun(options: ButtonOptions): Button
9-
Button = require(PATH .. ".Button"),
10-
ButtonGroup = require(PATH .. ".ButtonGroup"),
11-
Carousel = require(PATH .. ".Carousel"),
12-
---@see ChangeInputButton
13-
---@type fun(options: ChangeInputButtonOptions): ChangeInputButton
14-
ChangeInputButton = require(PATH .. ".ChangeInputButton"),
15-
Focusable = require(PATH .. ".Focusable"),
16-
FocusDirector = require(PATH .. ".FocusDirector"),
17-
Grid = require(PATH .. ".Grid"),
18-
GridCursor = require(PATH .. ".GridCursor"),
19-
---@see ImageButton
20-
---@type fun(options: ImageButtonOptions): ImageButton
21-
ImageButton = require(PATH .. ".ImageButton"),
22-
ImageContainer = require(PATH .. ".ImageContainer"),
23-
InputField = require(PATH .. ".InputField"),
24-
KeyBindingMenuItem = require(PATH .. ".KeyBindingMenuItem"),
25-
---@see Label
26-
---@type fun(options: LabelOptions): Label
27-
Label = require(PATH .. ".Label"),
28-
Leaderboard = require(PATH .. ".Leaderboard"),
29-
---@see LevelSlider
30-
---@type fun(options: SliderOptions): LevelSlider
31-
LevelSlider = require(PATH .. ".LevelSlider"),
32-
Menu = require(PATH .. ".Menu"),
33-
MenuItem = require(PATH .. ".MenuItem"),
34-
MultiPlayerSelectionWrapper = require(PATH .. ".MultiPlayerSelectionWrapper"),
35-
PagedUniGrid = require(PATH .. ".PagedUniGrid"),
36-
PanelCarousel = require(PATH .. ".PanelCarousel"),
37-
---@see PixelFontLabel
38-
---@type fun(options: PixelFontLabelOptions): PixelFontLabel
39-
PixelFontLabel = require(PATH .. ".PixelFontLabel"),
40-
---@see ScrollContainer
41-
---@type fun(options: ScrollContainerOptions): ScrollContainer
42-
ScrollContainer = require(PATH .. ".ScrollContainer"),
43-
ScrollText = require(PATH .. ".ScrollText"),
44-
---@see Slider
45-
---@type fun(options: SliderOptions): Slider
46-
Slider = require(PATH .. ".Slider"),
47-
SliderMenuItem = require(PATH .. ".SliderMenuItem"),
27+
---@source BoolSelector.lua
28+
BoolSelector = import("./BoolSelector"),
29+
---@source Button.lua
30+
Button = import("./Button"),
31+
---@source ButtonGroup.lua
32+
ButtonGroup = import("./ButtonGroup"),
33+
---@source Carousel.lua
34+
Carousel = import("./Carousel"),
35+
---@source ChangeInputButton.lua
36+
ChangeInputButton = import("./ChangeInputButton"),
37+
---@source Focusable.lua
38+
Focusable = import("./Focusable"),
39+
---@source FocusDirector.lua
40+
FocusDirector = import("./FocusDirector"),
41+
---@source Grid.lua
42+
Grid = import("./Grid"),
43+
---@source GridCursor.lua
44+
GridCursor = import("./GridCursor"),
45+
---@source ImageButton.lua
46+
ImageButton = import("./ImageButton"),
47+
---@source ImageContainer.lua
48+
ImageContainer = import("./ImageContainer"),
49+
---@source InputField.lua
50+
InputField = import("./InputField"),
51+
---@source KeyBindingMenuItem.lua
52+
KeyBindingMenuItem = import("./KeyBindingMenuItem"),
53+
---@source Label.lua
54+
Label = import("./Label"),
55+
---@source Leaderboard.lua
56+
Leaderboard = import("./Leaderboard"),
57+
---@source LevelSlider.lua
58+
LevelSlider = import("./LevelSlider"),
59+
---@source Menu.lua
60+
Menu = import("./Menu"),
61+
---@source MenuItem.lua
62+
MenuItem = import("./MenuItem"),
63+
---@source MultiPlayerSelectionWrapper.lua
64+
MultiPlayerSelectionWrapper = import("./MultiPlayerSelectionWrapper"),
65+
---@source PagedUniGrid.lua
66+
PagedUniGrid = import("./PagedUniGrid"),
67+
---@source PanelCarousel.lua
68+
PanelCarousel = import("./PanelCarousel"),
69+
---@source PixelFontLabel.lua
70+
PixelFontLabel = import("./PixelFontLabel"),
71+
---@source ScrollContainer.lua
72+
ScrollContainer = import("./ScrollContainer"),
73+
---@source ScrollText.lua
74+
ScrollText = import("./ScrollText"),
75+
---@source Slider.lua
76+
Slider = import("./Slider"),
77+
---@source SliderMenuItem.lua
78+
SliderMenuItem = import("./SliderMenuItem"),
4879
---@source StackElement.lua
49-
StackElement = require(PATH .. ".StackElement"),
50-
StackPanel = require(PATH .. ".StackPanel"),
51-
StageCarousel = require(PATH .. ".StageCarousel"),
52-
Stepper = require(PATH .. ".Stepper"),
53-
---@see TextButton
54-
---@type fun(options: TextButtonOptions): TextButton
55-
TextButton = require(PATH .. ".TextButton"),
56-
---@see UiElement
57-
---@type fun(options:UiElementOptions): UiElement
58-
UiElement = require(PATH .. ".UIElement"),
59-
ValueLabel = require(PATH .. ".ValueLabel"),
80+
StackElement = import("./StackElement"),
81+
---@source StackPanel.lua
82+
StackPanel = import("./StackPanel"),
83+
---@source StageCarousel.lua
84+
StageCarousel = import("./StageCarousel"),
85+
---@source Stepper.lua
86+
Stepper = import("./Stepper"),
87+
---@source TextButton.lua
88+
TextButton = import("./TextButton"),
89+
---@source UiElement.lua
90+
UiElement = import("./UIElement"),
91+
---@source ValueLabel.lua
92+
ValueLabel = import("./ValueLabel"),
6093
}
6194

6295
return ui

common/lib/import.lua

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
--[[
2+
A library to provide a relative require.
3+
Makes sense to use wherever we have grouped files that assuredly only ever move together.
4+
Otherwise require is probably still better.
5+
6+
MIT License
7+
8+
Copyright (c) 2023 Justin van der Leij
9+
10+
Permission is hereby granted, free of charge, to any person obtaining a copy
11+
of this software and associated documentation files (the "Software"), to deal
12+
in the Software without restriction, including without limitation the rights
13+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
copies of the Software, and to permit persons to whom the Software is
15+
furnished to do so, subject to the following conditions:
16+
17+
The above copyright notice and this permission notice shall be included in all
18+
copies or substantial portions of the Software.
19+
20+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26+
SOFTWARE.
27+
]]
28+
29+
local function extractPathComponents(path)
30+
local components = {}
31+
for component in path:gmatch("[^/]+") do
32+
table.insert(components, component)
33+
end
34+
35+
return components
36+
end
37+
38+
local import = function(path)
39+
local callerPath = debug.getinfo(2, "S").source:sub(2)
40+
41+
local pathStack = {}
42+
43+
if (path:sub(1, 1) == ".") then
44+
local components = extractPathComponents(callerPath)
45+
46+
for i = 1, #components - 1 do
47+
pathStack[i] = components[i]
48+
end
49+
end
50+
51+
local components = extractPathComponents(path)
52+
53+
for _, component in ipairs(components) do
54+
if (component == ".") then
55+
-- Skip
56+
elseif (component == "..") then
57+
table.remove(pathStack, #pathStack)
58+
else
59+
table.insert(pathStack, component)
60+
end
61+
end
62+
63+
local out = table.concat(pathStack, ".")
64+
65+
return require(out)
66+
end
67+
68+
return import

common/lib/luaLsPlugin.lua

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
--[[
2+
A plugin for the language server to resolve the import libraries requires
3+
Intellisense becomes available for the returned types despite them technically being just a lua function call
4+
5+
MIT License
6+
7+
Copyright (c) 2024 Elmārs Āboliņš, including code from Justin van der Leij
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a copy
10+
of this software and associated documentation files (the "Software"), to deal
11+
in the Software without restriction, including without limitation the rights
12+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
copies of the Software, and to permit persons to whom the Software is
14+
furnished to do so, subject to the following conditions:
15+
16+
The above copyright notice and this permission notice shall be included in all
17+
copies or substantial portions of the Software.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25+
SOFTWARE.
26+
]]
27+
28+
local workspace = require "workspace"
29+
30+
local function extractPathComponents(path)
31+
local components = {}
32+
for component in path:gmatch("[^/]+") do
33+
table.insert(components, component)
34+
end
35+
36+
return components
37+
end
38+
39+
local import = function(path, fileUri)
40+
local callerPath = fileUri
41+
42+
local pathStack = {}
43+
44+
if (path:sub(1, 1) == ".") then
45+
local components = extractPathComponents(callerPath)
46+
47+
for i = 1, #components - 1 do
48+
pathStack[i] = components[i]
49+
end
50+
end
51+
52+
local components = extractPathComponents(path)
53+
54+
for _, component in ipairs(components) do
55+
if (component == ".") then
56+
-- Skip
57+
elseif (component == "..") then
58+
table.remove(pathStack, #pathStack)
59+
else
60+
table.insert(pathStack, component)
61+
end
62+
end
63+
64+
local out = table.concat(pathStack, ".")
65+
66+
return "require(\""..out.."\")"
67+
end
68+
69+
function OnSetText(uri, text)
70+
local diffs = {}
71+
72+
local transformedUri = uri:sub(#workspace.rootUri+2)
73+
74+
for startPos, path, finish in text:gmatch '()import%(([^%(%)]+)%)()' do
75+
diffs[#diffs+1] = {
76+
start = startPos,
77+
finish = finish - 1,
78+
text = import(path:gsub('\"',""), transformedUri),
79+
}
80+
end
81+
82+
return diffs
83+
end

0 commit comments

Comments
 (0)