diff --git a/src/BizHawk.Client.Common/lua/Documentation/LuaCatsGenerator.cs b/src/BizHawk.Client.Common/lua/Documentation/LuaCatsGenerator.cs new file mode 100644 index 00000000000..906a2b7f471 --- /dev/null +++ b/src/BizHawk.Client.Common/lua/Documentation/LuaCatsGenerator.cs @@ -0,0 +1,294 @@ +#nullable enable + +#pragma warning disable MA0136 // Raw String contains an implicit end of line character, line endings will be normalized + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using BizHawk.Common; +using BizHawk.Common.ReflectionExtensions; +using NLua; + +namespace BizHawk.Client.Common; + +/// +/// Generates API definitions in the LuaCATS format. +/// +/// +/// See https://luals.github.io/wiki/annotations +/// +internal static class LuaCatsGenerator +{ + private static readonly Dictionary TypeConversions = new() + { + [typeof(object)] = "any", + [typeof(byte)] = "integer", + [typeof(sbyte)] = "integer", + [typeof(int)] = "integer", + [typeof(uint)] = "integer", + [typeof(short)] = "integer", + [typeof(ushort)] = "integer", + [typeof(long)] = "integer", + [typeof(ulong)] = "integer", + [typeof(float)] = "number", + [typeof(double)] = "number", + [typeof(decimal)] = "number", + [typeof(string)] = "string", + [typeof(bool)] = "boolean", + [typeof(byte[])] = "string", + [typeof(Memory)] = "string", + [typeof(ReadOnlyMemory)] = "string", + [typeof(LuaFunction)] = "function", + [typeof(LuaTable)] = "table", + [typeof(System.Drawing.Color)] = "dotnetcolor", + }; + + private const string Classes = """ +---@class dotnetcolor : userdata + +---A color in one of the following formats: +--- - Number in the format `0xAARRGGBB` +--- - String in the format `"#RRGGBB"` or `"#AARRGGBB"` +--- - A CSS3/X11 color name e.g. `"blue"`, `"palegoldenrod"` +--- - Color created with `forms.createcolor` +---@alias color dotnetcolor | integer | string + +---@alias surface +---| "emucore" # Draw on the emulated screen. Resolution depends on emulated system and game. Drawing is scaled with the rest of the display. +---| "client" # Draw on the BizHawk window. Resolution depends on the window size. Drawing is not scaled. +"""; + + private const string Preamble = """ +-- https://tasvideos.org/Bizhawk + +error("This is a definition file for Lua Language Server and not a usable script") + +---@meta _ +"""; + + private static string? GetHardcodedType(ParameterInfo parameter) + { + // Technically any string parameter can be passed a number in BizHawk's Lua API, but let's just focus on the ones where it's commonly used + // like `gui.text` and `forms.settext` instead of polluting the entire API surface + if (parameter.Name is "message" or "caption" && parameter.ParameterType == typeof(string)) + { + return "string|number"; + } + + if (parameter.Member.DeclaringType.Name == "GuiLuaLibrary" && parameter.Name == "surfaceName" && parameter.ParameterType == typeof(string)) + { + return "surface"; + } + + return null; + } + + public static void Generate(LuaDocumentation docs, string path) + { + var sb0 = new StringBuilder(); + + sb0.AppendLine($"-- Lua functions available in EmuHawk {VersionInfo.MainVersion}"); + sb0.AppendLine(Preamble); + sb0.AppendLine(); + sb0.AppendLine(Classes); + sb0.AppendLine(); + + File.WriteAllText(Path.Combine(path, "classes.d.lua"), sb0.ToString().ReplaceLineEndings()); + + foreach (var libraryGroup in docs.GroupBy(func => func.Library).OrderBy(group => group.Key)) + { + string library = libraryGroup.Key; + string libraryDescription = libraryGroup.First().LibraryDescription; + var libraryType = libraryGroup.First().Method.DeclaringType; + var filePath = Path.Combine(path, library + ".d.lua"); + var sb = new StringBuilder(); + + sb.AppendLine($"-- Lua functions available in EmuHawk {VersionInfo.MainVersion}"); + sb.AppendLine(Preamble); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(libraryDescription)) + { + sb.AppendLine(FormatMarkdown(libraryDescription)); + } + + sb.AppendLine($"---@class {SafeLibraryTypeName(library)}"); + if (!typeof(LuaLibraryBase).IsAssignableFrom(libraryType)) sb.Append("local "); // don't make LuaCanvas global + sb.AppendLine($"{library} = {{}}"); + sb.AppendLine(); + + foreach (var func in libraryGroup.OrderBy(func => func.Name)) + { + if (!string.IsNullOrEmpty(func.Description)) + { + sb.AppendLine(FormatMarkdown(func.Description)); + } + + if (func.Example != null) + { + sb.AppendLine("---"); + sb.AppendLine("---Example:"); + sb.AppendLine("---"); + sb.AppendLine(FormatMarkdown(func.Example, "---\t")); + } + + if (func.IsDeprecated) + { + sb.AppendLine("---@deprecated"); + } + + foreach (var parameter in func.Method.GetParameters()) + { + if (IsParams(parameter)) + { + sb.Append("---@vararg"); + } + else + { + sb.Append($"---@param {parameter.Name}"); + if (parameter.HasDefaultValue || (parameter.IsNRTOrNullableT() ?? true)) + { + sb.Append('?'); + } + } + + sb.Append(' '); + sb.Append(GetLuaType(parameter)); + if (IsZeroIndexed(parameter)) + { + sb.Append(" Zero-indexed array."); + } + if (parameter.HasDefaultValue && parameter.DefaultValue is not null and not "") + { + sb.Append($" Defaults to `{FormatValue(parameter.DefaultValue)}`"); + } + sb.AppendLine(); + } + + if (func.Method.ReturnType != typeof(void)) + { + sb.Append("---@return "); + var luaType = GetLuaType(func.Method.ReturnParameter); + var nilable = func.Method.ReturnParameter.IsNRTOrNullableT() ?? true; + var wrapType = nilable && luaType.IndexOfAny([ ':', '|' ]) != -1; // ? is ambiguous on complex types like `string|int` or `fun(): string` + if (wrapType) sb.Append('('); + sb.Append(luaType); + if (wrapType) sb.Append(')'); + if (nilable) sb.Append('?'); + if (IsZeroIndexed(func.Method.ReturnParameter)) + { + sb.Append(" # Zero-indexed array."); + } + sb.AppendLine(); + } + + sb.Append($"function {library}.{func.Name}("); + + foreach (var parameter in func.Method.GetParameters()) + { + if (parameter.Position > 0) + { + sb.Append(", "); + } + sb.Append(IsParams(parameter) ? "..." : parameter.Name); + } + + sb.AppendLine(") end"); + sb.AppendLine(); + } + File.WriteAllText(filePath, sb.ToString().ReplaceLineEndings()); + } + } + + private static string FormatMarkdown(string value, string prefix = "---") + { + // prefix every line + value = Regex.Replace(value, "^", prefix, RegexOptions.Multiline); + // replace {{wiki markup}} with `markdown` + value = Regex.Replace(value, "{{(.+?)}}", "`$1`"); + // replace wiki image markup with markdown + value = Regex.Replace(value, @"\[(?.+?)\|alt=(?.+?)\]", "![${alt}](${url})"); + return value; + } + + private static string FormatValue(object value) => value switch + { + string str => $"\"{str}\"", + true => "true", + false => "false", + null => "nil", + _ => value.ToString(), + }; + + /// + /// Avoid name collisions with existing Lua types. + /// Only for the @class annotation, not the name of the global. + /// + /// + private static string SafeLibraryTypeName(string name) => name switch + { + "userdata" => $"biz{name}", + _ => name, + }; + + private static string GetLuaType(ParameterInfo parameter) + { + if (GetOverrideType(parameter) is string overrideType) + { + return overrideType; + } + + if (GetHardcodedType(parameter) is string hardcodedType) + { + return hardcodedType; + } + + if (parameter.GetCustomAttribute() is not null) + { + return "color"; // see Preamble + } + + if (parameter.ParameterType.IsArray && IsParams(parameter)) + { + // no [] array modifier for varargs + return GetLuaType(parameter.ParameterType.GetElementType()); + } + + return GetLuaType(parameter.ParameterType); + } + + private static string GetLuaType(Type type) + { + // try this twice, before and after extracting the array/nullable type + if (TypeConversions.TryGetValue(type, out string luaType)) + { + return luaType; + } + + if (type.IsArray) + { + return GetLuaType(type.GetElementType()) + "[]"; + } + + if (Nullable.GetUnderlyingType(type) is Type underlyingType) + { + type = underlyingType; + } + + if (TypeConversions.TryGetValue(type, out luaType)) + { + return luaType; + } + + throw new NotSupportedException($"Unknown type {type.FullName} used in API. Generator must be updated to handle this."); + } + + private static bool IsParams(ParameterInfo parameter) => parameter.GetCustomAttribute() is not null; + + private static bool IsZeroIndexed(ParameterInfo parameter) => parameter.GetCustomAttribute() is not null; + + private static string? GetOverrideType(ParameterInfo parameter) => parameter.GetCustomAttribute()?.Type; +} diff --git a/src/BizHawk.Client.Common/lua/LuaAttributes.cs b/src/BizHawk.Client.Common/lua/LuaAttributes.cs index ffa75dd81d0..cc9624f923c 100644 --- a/src/BizHawk.Client.Common/lua/LuaAttributes.cs +++ b/src/BizHawk.Client.Common/lua/LuaAttributes.cs @@ -44,4 +44,14 @@ public LuaLibraryAttribute(bool released) /// [AttributeUsage(AttributeTargets.ReturnValue | AttributeTargets.Parameter)] public sealed class LuaZeroIndexedAttribute : Attribute { } + + /// + /// Specifies a custom type to be used in LuaCATS annotations, for documentation purposes only. + /// See and for possible values. + /// + [AttributeUsage(AttributeTargets.ReturnValue | AttributeTargets.Parameter)] + public sealed class LuaCatsTypeAttribute(string Type) : Attribute + { + public string Type { get; } = Type; + } } diff --git a/src/BizHawk.Client.Common/lua/LuaDocumentation.cs b/src/BizHawk.Client.Common/lua/LuaDocumentation.cs index 2174d646916..eff42860d74 100644 --- a/src/BizHawk.Client.Common/lua/LuaDocumentation.cs +++ b/src/BizHawk.Client.Common/lua/LuaDocumentation.cs @@ -199,6 +199,11 @@ public string ToNotepadPlusPlusAutoComplete() { return ""; // TODO } + + public void ToLuaLanguageServerDefinitions(string path) + { + LuaCatsGenerator.Generate(this, path); + } } public class LibraryFunction diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs index 4c06f27caa2..17583a1b9ae 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs @@ -360,6 +360,7 @@ public void Text( description: "Creates a dedicated canvas window, returning a table containing some callbacks for drawing. These are the LuaCanvas functions in the API reference." + " The width and height parameters determine the size of the canvas." + " If the x and y parameters are both nil/unset, the form (window) will appear at the default position. If both are specified, the form will be positioned at (x, y) on the screen.")] // technically x can be specified w/o y but let's leave that as UB + [return: LuaCatsType("LuaCanvas")] public LuaTable CreateCanvas(int width, int height, int? x = null, int? y = null) { var canvas = new LuaCanvas(APIs.Emulation, PathEntries, width, height, x, y, _th, LogOutputCallback); diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaAutocompleteInstaller.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaAutocompleteInstaller.cs index c2b6878b186..aa18d23d1c4 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaAutocompleteInstaller.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaAutocompleteInstaller.cs @@ -46,17 +46,19 @@ public void InstallBizLua(TextEditors editor, LuaDocumentation docs) private string AppDataFolder => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + private string ProgramFilesFolder => Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + private bool IsSublimeInstalled() { // The most likely location of the app, eventually we should consider looking through the registry or installed apps as a more robust way to detect it; - string exePath = @"C:\Program Files\Sublime Text 2\sublime_text.exe"; + string exePath = Path.Combine(ProgramFilesFolder, @"Sublime Text 2\sublime_text.exe"); return File.Exists(exePath); } private bool IsNotepadInstalled() { // The most likely location of the app, eventually we should consider looking through the registry or installed apps as a more robust way to detect it; - string exePath = @"C:\Program Files (x86)\Notepad++\notepad++.exe"; + string exePath = Path.Combine(ProgramFilesFolder, @"Notepad++\notepad++.exe"); return File.Exists(exePath); } diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs index 53f366891db..23646d0f481 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs @@ -73,8 +73,9 @@ private void InitializeComponent() this.DisableScriptsOnLoadMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); this.ReturnAllIfNoneSelectedMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); this.ReloadWhenScriptFileChangesMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); - this.toolStripSeparator4 = new BizHawk.WinForms.Controls.ToolStripSeparatorEx(); - this.RegisterToTextEditorsSubMenu = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); + this.integrationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.GenerateLuaCatsDefinitionMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); + this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); this.RegisterSublimeText2MenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); this.RegisterNotePadMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); this.HelpSubMenu = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); @@ -177,6 +178,7 @@ private void InitializeComponent() this.FileSubMenu, this.ScriptSubMenu, this.SettingsSubMenu, + this.integrationToolStripMenuItem, this.HelpSubMenu}); this.menuStrip1.TabIndex = 1; // @@ -348,9 +350,7 @@ private void InitializeComponent() this.SettingsSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.DisableScriptsOnLoadMenuItem, this.ReturnAllIfNoneSelectedMenuItem, - this.ReloadWhenScriptFileChangesMenuItem, - this.toolStripSeparator4, - this.RegisterToTextEditorsSubMenu}); + this.ReloadWhenScriptFileChangesMenuItem}); this.SettingsSubMenu.Text = "&Settings"; this.SettingsSubMenu.DropDownOpened += new System.EventHandler(this.OptionsSubMenu_DropDownOpened); // @@ -369,22 +369,36 @@ private void InitializeComponent() this.ReloadWhenScriptFileChangesMenuItem.Text = "Reload When Script File Changes"; this.ReloadWhenScriptFileChangesMenuItem.Click += new System.EventHandler(this.ReloadWhenScriptFileChangesMenuItem_Click); // - // RegisterToTextEditorsSubMenu + // integrationToolStripMenuItem // - this.RegisterToTextEditorsSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.integrationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.GenerateLuaCatsDefinitionMenuItem, + this.toolStripSeparator1, this.RegisterSublimeText2MenuItem, this.RegisterNotePadMenuItem}); - this.RegisterToTextEditorsSubMenu.Text = "Register To Text Editors"; - this.RegisterToTextEditorsSubMenu.DropDownOpened += new System.EventHandler(this.RegisterToTextEditorsSubMenu_DropDownOpened); + this.integrationToolStripMenuItem.Name = "integrationToolStripMenuItem"; + this.integrationToolStripMenuItem.Size = new System.Drawing.Size(82, 20); + this.integrationToolStripMenuItem.Text = "&Integration"; + this.integrationToolStripMenuItem.DropDownOpened += new System.EventHandler(this.IntegrationMenu_DropDownOpened); + // + // GenerateLuaCatsDefinitionMenuItem + // + this.GenerateLuaCatsDefinitionMenuItem.Text = "Export &LuaCATS (Lua Language Server) File"; + this.GenerateLuaCatsDefinitionMenuItem.Click += new System.EventHandler(this.GenerateLuaCatsDefinitionMenuItem_Click); + // + // toolStripSeparator1 + // + this.toolStripSeparator1.Name = "toolStripSeparator1"; + this.toolStripSeparator1.Size = new System.Drawing.Size(322, 6); // // RegisterSublimeText2MenuItem // - this.RegisterSublimeText2MenuItem.Text = "&Sublime Text 2"; + this.RegisterSublimeText2MenuItem.Text = "Register to &Sublime Text 2"; this.RegisterSublimeText2MenuItem.Click += new System.EventHandler(this.RegisterSublimeText2MenuItem_Click); // // RegisterNotePadMenuItem // - this.RegisterNotePadMenuItem.Text = "Notepad++"; + this.RegisterNotePadMenuItem.Text = "Register to &Notepad++"; this.RegisterNotePadMenuItem.Click += new System.EventHandler(this.RegisterNotePadMenuItem_Click); // // HelpSubMenu @@ -786,14 +800,15 @@ private void InitializeComponent() private System.Windows.Forms.SplitContainer splitContainer1; private BizHawk.WinForms.Controls.ToolStripMenuItemEx ReturnAllIfNoneSelectedMenuItem; private BizHawk.WinForms.Controls.ToolStripMenuItemEx ReloadWhenScriptFileChangesMenuItem; - private BizHawk.WinForms.Controls.ToolStripSeparatorEx toolStripSeparator4; - private BizHawk.WinForms.Controls.ToolStripMenuItemEx RegisterToTextEditorsSubMenu; - private BizHawk.WinForms.Controls.ToolStripMenuItemEx RegisterSublimeText2MenuItem; - private BizHawk.WinForms.Controls.ToolStripMenuItemEx RegisterNotePadMenuItem; private BizHawk.WinForms.Controls.ToolStripMenuItemEx SelectAllContextItem; private BizHawk.WinForms.Controls.ToolStripMenuItemEx CopyContextItem; private BizHawk.WinForms.Controls.ToolStripMenuItemEx ClearRegisteredFunctionsContextItem; private BizHawk.WinForms.Controls.ToolStripSeparatorEx toolStripSeparator5; private BizHawk.WinForms.Controls.ToolStripMenuItemEx ClearRegisteredFunctionsLogContextItem; + private System.Windows.Forms.ToolStripMenuItem integrationToolStripMenuItem; + private ToolStripMenuItemEx GenerateLuaCatsDefinitionMenuItem; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator1; + private ToolStripMenuItemEx RegisterSublimeText2MenuItem; + private ToolStripMenuItemEx RegisterNotePadMenuItem; } } \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs index ff9ea53a77d..9d485f63be7 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs @@ -1145,7 +1145,7 @@ private void ReloadWhenScriptFileChangesMenuItem_Click(object sender, EventArgs } } - private void RegisterToTextEditorsSubMenu_DropDownOpened(object sender, EventArgs e) + private void IntegrationMenu_DropDownOpened(object sender, EventArgs e) { // Hide until this one is implemented RegisterNotePadMenuItem.Visible = false; @@ -1205,6 +1205,38 @@ private void RegisterNotePadMenuItem_Click(object sender, EventArgs e) _luaAutoInstaller.InstallBizLua(LuaAutocompleteInstaller.TextEditors.NotePad, LuaImp.Docs); } + private void GenerateLuaCatsDefinitionMenuItem_Click(object sender, EventArgs e) + { + DialogResult result; + string selectedPath; + string description = "Set the directory for LuaCATS definitions"; + if (OSTailoredCode.IsUnixHost) + { + // FolderBrowserEx doesn't work in Mono for obvious reasons + using var f = new FolderBrowserDialog + { + Description = description, + SelectedPath = Config!.PathEntries.LuaAbsolutePath(), + }; + result = f.ShowDialog(); + selectedPath = f.SelectedPath; + } + else + { + using var f = new FolderBrowserEx + { + Description = description, + SelectedPath = Config!.PathEntries.LuaAbsolutePath(), + }; + result = f.ShowDialog(); + selectedPath = f.SelectedPath; + } + if (result.IsOk()) + { + LuaImp.Docs.ToLuaLanguageServerDefinitions(selectedPath); + } + } + private void FunctionsListMenuItem_Click(object sender, EventArgs e) { new LuaFunctionsForm(LuaImp.Docs).Show();