diff --git a/__tests__/api-writer/glua-api-writer.spec.ts b/__tests__/api-writer/glua-api-writer.spec.ts index 3f58d62..05cf95e 100644 --- a/__tests__/api-writer/glua-api-writer.spec.ts +++ b/__tests__/api-writer/glua-api-writer.spec.ts @@ -217,7 +217,7 @@ describe('GLua API Writer', () => { url: 'https://wiki.facepunch.com/gmod/Structures/EntityStruct', }); - expect(api).toContain('---@field entity Entity="NULL"'); + expect(api).toContain('---@field entity Entity=NULL'); }); it('should be able to write Lua API definitions directly from wiki json data for a fake enum', async () => { @@ -236,7 +236,7 @@ describe('GLua API Writer', () => { const api = writer.makeApiFromPages(writer.getPages(mockFilePath)); expect(api).toContain('---@realm server'); expect(api).toContain('---@source https://wiki.facepunch.com/gmod/Enums/NavCorner'); - expect(api).toContain('--- @alias NavCorner 0 | 1 | 2 | 3 | 4 | number'); + expect(api).toContain('---@alias NavCorner 0 | 1 | 2 | 3 | 4 | number'); }); it('should handle deprecated in description', async () => { diff --git a/__tests__/api-writer/write-type.spec.ts b/__tests__/api-writer/write-type.spec.ts index ee855bf..e2d31ae 100644 --- a/__tests__/api-writer/write-type.spec.ts +++ b/__tests__/api-writer/write-type.spec.ts @@ -32,4 +32,40 @@ describe('writeType', () => { expect(result).toEqual('fun(count: number, arg1: string):(ret0: number, ret1: string)'); }); }); + + describe('sequential tables', () => { + it('should convert table to X[]', async () => { + expect(GluaApiWriter.transformType('table')).toEqual('Player[]'); + }); + + it('should strip the Structures page path from the element type', async () => { + expect(GluaApiWriter.transformType('table')).toEqual('LocalLight[]'); + }); + + it('should convert a nested element type', async () => { + expect(GluaApiWriter.transformType('table')).toEqual('Undo[]'); + }); + + it('should leave table untouched', async () => { + expect(GluaApiWriter.transformType('table')).toEqual('table'); + }); + }); + + describe('unions', () => { + it('should keep nil alongside a converted sequential table', async () => { + expect(GluaApiWriter.transformType('table|nil')).toEqual('Sky3DParams[]|nil'); + }); + + it('should keep nil alongside a converted struct table', async () => { + expect(GluaApiWriter.transformType('table{AngPos}|nil')).toEqual('AngPos|nil'); + }); + + it('should convert members that are not first', async () => { + expect(GluaApiWriter.transformType('string|table{FormattedTime}')).toEqual('string|FormattedTime'); + }); + + it('should leave plain unions untouched', async () => { + expect(GluaApiWriter.transformType('table|boolean|nil')).toEqual('table|boolean|nil'); + }); + }); }); diff --git a/custom/HTTPRequest.lua b/custom/HTTPRequest.lua index bd933ff..281032d 100644 --- a/custom/HTTPRequest.lua +++ b/custom/HTTPRequest.lua @@ -56,9 +56,7 @@ --- * DELETE --- * PATCH --- * OPTIONS ---- ----Default: `GET` ----@field method? string +---@field method string="GET" ---The target url. ---@field url string ---KeyValue table for [URL parameters](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams). @@ -72,22 +70,16 @@ --- Supported by methods such as `POST`, `PUT`, `PATCH`, and `DELETE`. ---@field body? string ---Content type for body. ---- ----Default: `text/plain; charset=utf-8` ----@field type? string +---@field type string="text/plain; charset=utf-8" ---The timeout for the connection. ---- ----Default: `60` ----@field timeout? number +---@field timeout number=60 local HTTPRequest = {} ---`GET`, `POST`, and `HEAD` requests may include URL parameters. --- Omitting `method` is treated as `GET`. ---@class (exact) HTTPRequestWithParameters : HTTPRequest ---Request method, case insensitive. ---- ----Default: `GET` ----@field method? HTTPRequestMethodWithParameters +---@field method HTTPRequestMethodWithParameters="GET" ---KeyValue table for [URL parameters](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams). --- --- Valid only for `GET`, `POST`, and `HEAD`. diff --git a/custom/ServerQueryData.lua b/custom/ServerQueryData.lua index 268a638..b147302 100644 --- a/custom/ServerQueryData.lua +++ b/custom/ServerQueryData.lua @@ -3,15 +3,11 @@ ---@source https://wiki.facepunch.com/gmod/Structures/ServerQueryData ---@class (partial) ServerQueryData ---The game directory to get the servers for. ---- ---- Default: `garrysmod` ----@field GameDir string +---@field GameDir string="garrysmod" ---Type of servers to retrieve. Valid values are `internet`, `favorite`, `history` and `lan`. ---@field Type string ---Steam application ID to get the servers for. ---- ---- Default: `4000` ----@field AppID number +---@field AppID number=4000 ---Called when a new server is found and queried. ---@field Callback fun(ping: number, name: string, desc: string, map: string, players: number, maxplayers: number, botplayers: number, pass: boolean, lastplayed: number, address: string, gamemode: string, workshopid: number, isanon: boolean, netversion: string, luaversion: string, localization: string, gmcategory: string):(stop: boolean) ---Called if the query has failed, called with the server IP address. diff --git a/custom/_globals.lua b/custom/_globals.lua index 49ddc46..ed7d8df 100644 --- a/custom/_globals.lua +++ b/custom/_globals.lua @@ -1,6 +1,6 @@ ---@meta ---- Source: https://wiki.facepunch.com/gmod/Global_Variables +---@source https://wiki.facepunch.com/gmod/Global_Variables --[[ Global Tables diff --git a/custom/class.CreationMenu.lua b/custom/class.CreationMenu.lua new file mode 100644 index 0000000..7db3fb7 --- /dev/null +++ b/custom/class.CreationMenu.lua @@ -0,0 +1,24 @@ +--- The content half of the spawn menu, holding every tab registered with +--- [spawnmenu.AddCreationTab](https://wiki.facepunch.com/gmod/spawnmenu.AddCreationTab). +---@source garrysmod/gamemodes/sandbox/gamemode/spawnmenu/creationmenu.lua +---@class CreationMenu : DPropertySheet +---@field CreationTabs table The created tabs, keyed by tab name. +local CreationMenu = {} + +---@class CreationMenuTab : DPropertySheetSheet +---@field ContentPanel? Panel The panel built by the tab's populate function. Only set once the tab has been populated. + +---Returns a single creation tab by name. +---@realm client +---@param id string The tab name, as passed to spawnmenu.AddCreationTab. +---@return CreationMenuTab? # The tab, or `nil` if no tab with that name exists. +function CreationMenu:GetCreationTab(id) end + +---Returns every creation tab on this menu. +---@realm client +---@return table # The created tabs, keyed by tab name. +function CreationMenu:GetCreationTabs() end + +---Creates a tab for every creation tab registered with spawnmenu.AddCreationTab. +---@realm client +function CreationMenu:Populate() end diff --git a/custom/class.DHorizontalDivider.lua b/custom/class.DHorizontalDivider.lua new file mode 100644 index 0000000..9cb19af --- /dev/null +++ b/custom/class.DHorizontalDivider.lua @@ -0,0 +1,4 @@ +---@class DHorizontalDivider : DPanel +---@field m_DragBar DHorizontalDividerBar The drag handle between the left and right panels. +---@field _OldCookieW number The last left width restored from cookies. +local DHorizontalDivider = {} diff --git a/custom/class.DHorizontalDividerBar.lua b/custom/class.DHorizontalDividerBar.lua new file mode 100644 index 0000000..0458f5e --- /dev/null +++ b/custom/class.DHorizontalDividerBar.lua @@ -0,0 +1,5 @@ +--- The drag handle of a [DHorizontalDivider](https://wiki.facepunch.com/gmod/DHorizontalDivider). +--- Created automatically by the divider and stored in its `m_DragBar` field. +---@source garrysmod/lua/vgui/dhorizontaldivider.lua +---@class DHorizontalDividerBar : DPanel +local DHorizontalDividerBar = {} diff --git a/custom/class.DVerticalDivider.lua b/custom/class.DVerticalDivider.lua new file mode 100644 index 0000000..9600a18 --- /dev/null +++ b/custom/class.DVerticalDivider.lua @@ -0,0 +1,3 @@ +---@class DVerticalDivider : DPanel +---@field m_DragBar DVerticalDividerBar The drag handle between the top and bottom panels. +local DVerticalDivider = {} diff --git a/custom/class.DVerticalDividerBar.lua b/custom/class.DVerticalDividerBar.lua new file mode 100644 index 0000000..3339d70 --- /dev/null +++ b/custom/class.DVerticalDividerBar.lua @@ -0,0 +1,5 @@ +--- The drag handle of a [DVerticalDivider](https://wiki.facepunch.com/gmod/DVerticalDivider). +--- Created automatically by the divider and stored in its `m_DragBar` field. +---@source garrysmod/lua/vgui/dverticaldivider.lua +---@class DVerticalDividerBar : DPanel +local DVerticalDividerBar = {} diff --git a/custom/class.DriveMethod.lua b/custom/class.DriveMethod.lua index 0bf2449..081b4a2 100644 --- a/custom/class.DriveMethod.lua +++ b/custom/class.DriveMethod.lua @@ -2,7 +2,7 @@ ---Runtime drive mode table returned by drive.GetMethod. --- ---- Source: https://github.com/Facepunch/garrysmod/blob/master/garrysmod/lua/drive/drive_base.lua +---@source https://github.com/Facepunch/garrysmod/blob/master/garrysmod/lua/drive/drive_base.lua ---@class DriveMethod ---@field Entity Entity Driven entity. ---@field Player Player Driving player. diff --git a/custom/class.GM.lua b/custom/class.GM.lua index 713ddeb..3717eaf 100644 --- a/custom/class.GM.lua +++ b/custom/class.GM.lua @@ -1,6 +1,5 @@ ---- Source: ---- - garrysmod/gamemodes/base/gamemode/shared.lua ---- - garrysmod/gamemodes/sandbox/gamemode/shared.lua +---@source garrysmod/gamemodes/base/gamemode/shared.lua +---@source garrysmod/gamemodes/sandbox/gamemode/shared.lua ---@class GM ---@field Name string Gamemode display name. ---@field Author string Gamemode author. diff --git a/custom/class.PlayerClass.lua b/custom/class.PlayerClass.lua index 15cf537..ecdc05d 100644 --- a/custom/class.PlayerClass.lua +++ b/custom/class.PlayerClass.lua @@ -15,20 +15,20 @@ ---@field ClassID? number Network string ID of the active player class. Injected at runtime by player_manager. ---@field Func? fun() Internal no-op placeholder. Injected at runtime by player_manager. ---@field DisplayName? string Human-readable display name for this player class. ----@field SlowWalkSpeed? number Movement speed when slow-walking (+WALK). Default: 200. ----@field WalkSpeed? number Movement speed when walking (not running). Default: 400. ----@field RunSpeed? number Movement speed when running. Default: 600. ----@field CrouchedWalkSpeed? number Multiplier applied to move speed while crouching. Default: 0.3. ----@field DuckSpeed? number Speed of transition from standing to crouching. Default: 0.3. ----@field UnDuckSpeed? number Speed of transition from crouching to standing. Default: 0.3. ----@field JumpPower? number Vertical impulse strength on jump. Default: 200. ----@field CanUseFlashlight? boolean Whether the player can use the flashlight. Default: true. ----@field MaxHealth? number Maximum health the player can have. Default: 100. ----@field MaxArmor? number Maximum armor the player can have. Default: 100. ----@field StartHealth? number Health given to the player on spawn. Default: 100. ----@field StartArmor? number Armor given to the player on spawn. Default: 0. ----@field DropWeaponOnDie? boolean Whether to drop the active weapon on death. Default: false. ----@field TeammateNoCollide? boolean Whether teammates pass through each other. Default: true. ----@field AvoidPlayers? boolean Whether the player auto-swerves around others. Default: true. ----@field UseVMHands? boolean Whether to use viewmodel hands. Default: true. +---@field SlowWalkSpeed number=200 Movement speed when slow-walking (+WALK). +---@field WalkSpeed number=400 Movement speed when walking (not running). +---@field RunSpeed number=600 Movement speed when running. +---@field CrouchedWalkSpeed number=0.3 Multiplier applied to move speed while crouching. +---@field DuckSpeed number=0.3 Speed of transition from standing to crouching. +---@field UnDuckSpeed number=0.3 Speed of transition from crouching to standing. +---@field JumpPower number=200 Vertical impulse strength on jump. +---@field CanUseFlashlight boolean=true Whether the player can use the flashlight. +---@field MaxHealth number=100 Maximum health the player can have. +---@field MaxArmor number=100 Maximum armor the player can have. +---@field StartHealth number=100 Health given to the player on spawn. +---@field StartArmor number=0 Armor given to the player on spawn. +---@field DropWeaponOnDie boolean=false Whether to drop the active weapon on death. +---@field TeammateNoCollide boolean=true Whether teammates pass through each other. +---@field AvoidPlayers boolean=true Whether the player auto-swerves around others. +---@field UseVMHands boolean=true Whether to use viewmodel hands. PlayerClass = {} diff --git a/custom/class.SKIN.lua b/custom/class.SKIN.lua index 63194ef..7e04507 100644 --- a/custom/class.SKIN.lua +++ b/custom/class.SKIN.lua @@ -1,6 +1,6 @@ ---@meta ---- Source: https://github.com/Facepunch/garrysmod/blob/b2bff902adf7f5b87ec543f873e74e3267e93f26/garrysmod/lua/skins/default.lua +---@source https://github.com/Facepunch/garrysmod/blob/b2bff902adf7f5b87ec543f873e74e3267e93f26/garrysmod/lua/skins/default.lua ---@class SKINColoursState ---@field Normal Color diff --git a/custom/class.TauntCamera.lua b/custom/class.TauntCamera.lua index 8828369..df388ff 100644 --- a/custom/class.TauntCamera.lua +++ b/custom/class.TauntCamera.lua @@ -1,6 +1,6 @@ --- A taunt camera object returned by [TauntCamera](https://wiki.facepunch.com/gmod/Global.TauntCamera). --- Used by player classes to drive a third-person taunt view. ---- Source: garrysmod/gamemodes/base/gamemode/player_class/taunt_camera.lua +---@source garrysmod/gamemodes/base/gamemode/player_class/taunt_camera.lua ---@class TauntCamera local TauntCamera = {} diff --git a/src/api-writer/glua-api-writer.ts b/src/api-writer/glua-api-writer.ts index 7eb944c..6353455 100644 --- a/src/api-writer/glua-api-writer.ts +++ b/src/api-writer/glua-api-writer.ts @@ -464,7 +464,7 @@ export class GluaApiWriter { const literalUnion = enumValues.join(' | '); const enumAliasValue = literalUnion.length > 0 ? `${literalUnion} | number` : 'number'; - api += `--- @alias ${_enum.name} ${enumAliasValue}\n`; + api += `---@alias ${_enum.name} ${enumAliasValue}\n`; } else { // Garry's Mod enums are flat globals, so the field list names each constant. // Completion then offers `EF_BONEMERGE` rather than the raw value it holds. @@ -502,13 +502,14 @@ export class GluaApiWriter { api += `---${wrapInComment(comment)}\n`; const type = GluaApiWriter.transformType(field.type, field.callback); - const { optional, inlineDefault } = this.getStructFieldDefaultAnnotation(field.default); + const { optional, inlineDefault } = this.getStructFieldDefaultAnnotation(field); api += `---@field ${GluaApiWriter.safeName(field.name)}${optional} ${type}${inlineDefault}\n`; return api; } - private getStructFieldDefaultAnnotation(defaultValue: string | undefined): { optional: string; inlineDefault: string } { + private getStructFieldDefaultAnnotation(field: Struct['fields'][number]): { optional: string; inlineDefault: string } { + const defaultValue = field.default; if (defaultValue === undefined) return { optional: '', inlineDefault: '' }; @@ -516,12 +517,12 @@ export class GluaApiWriter { if (trimmedDefault.toLowerCase() === 'nil') return { optional: '?', inlineDefault: '' }; - const normalizedDefault = this.normalizeInlineDefaultValue(trimmedDefault); + const normalizedDefault = this.normalizeInlineDefaultValue(trimmedDefault, field.type); return { optional: '', inlineDefault: `=${normalizedDefault}` }; } - private normalizeInlineDefaultValue(defaultValue: string): string { - if (/^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(defaultValue)) + private normalizeInlineDefaultValue(defaultValue: string, fieldType?: string): string { + if (/^[+-]?(?:0[xX][0-9a-fA-F]+|\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(defaultValue)) return defaultValue; if (/^(true|false)$/i.test(defaultValue)) @@ -531,13 +532,18 @@ export class GluaApiWriter { const boldMatch = unwrappedCode.match(/^\*\*(.*)\*\*$/); const normalizedText = (boldMatch ? boldMatch[1] : unwrappedCode).trim(); - if (normalizedText.toLowerCase() === 'empty') + if (normalizedText.toLowerCase() === 'empty' || normalizedText.toLowerCase() === '') return '""'; if (/^"(?:[^"\\]|\\.)*"$/.test(normalizedText) || /^'(?:[^'\\]|\\.)*'$/.test(normalizedText)) return normalizedText; - return JSON.stringify(normalizedText); + const isStringType = fieldType !== undefined && fieldType.trim().toLowerCase() === 'string'; + if (isStringType) { + return JSON.stringify(normalizedText); + } + + return normalizedText; } private writeStruct(struct: Struct) { @@ -830,10 +836,14 @@ export class GluaApiWriter { } } - public static transformType(type: string, callback?: FunctionCallback) { + public static transformType(type: string, callback?: FunctionCallback): string { if (type === 'vararg') return 'any'; + // Transform each member of a union separately, so `table|nil` keeps its `nil` + if (type.includes('|')) + return type.split('|').map(member => GluaApiWriter.transformType(member, callback)).join('|'); + // Convert `function` type to `fun(cmd: string, args: string):(returnValueName: string[]?)` if (type === 'function' && callback) { let callbackString = `fun(`; @@ -893,7 +903,9 @@ export class GluaApiWriter { if (!innerType) throw new Error(`Invalid table type: ${type}`); - return `${innerType}[]`; + // The wiki writes struct element types as page paths (`table`) + // and can nest its own syntax inside (`table`) + return `${GluaApiWriter.transformType(innerType.replace(/^Structures\//, ''))}[]`; } else if (type.startsWith('table{') || type.startsWith('Panel{')) { // Convert `table{ToScreenData}` structures to `ToScreenData` class for LuaLS // Also converts `Panel{DVScrollBar}` to `DVScrollBar` class for LuaLS