diff --git a/Markdown.Avalonia.SyntaxHigh/Markdown.Avalonia.SyntaxHigh.csproj b/Markdown.Avalonia.SyntaxHigh/Markdown.Avalonia.SyntaxHigh.csproj new file mode 100644 index 000000000..bf78cc72f --- /dev/null +++ b/Markdown.Avalonia.SyntaxHigh/Markdown.Avalonia.SyntaxHigh.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/Markdown.Avalonia.SyntaxHigh/StyleSetup.cs b/Markdown.Avalonia.SyntaxHigh/StyleSetup.cs new file mode 100644 index 000000000..781437baa --- /dev/null +++ b/Markdown.Avalonia.SyntaxHigh/StyleSetup.cs @@ -0,0 +1,11 @@ +using Avalonia.Styling; + +namespace Markdown.Avalonia.SyntaxHigh; + +public class StyleSetup +{ + public IEnumerable>> GetOverrideStyles() + { + return new List>>(); + } +} diff --git a/Markdown.Avalonia.SyntaxHigh/SyntaxSetup.cs b/Markdown.Avalonia.SyntaxHigh/SyntaxSetup.cs new file mode 100644 index 000000000..d337ac364 --- /dev/null +++ b/Markdown.Avalonia.SyntaxHigh/SyntaxSetup.cs @@ -0,0 +1,78 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Media; +using System.Text.RegularExpressions; + +namespace Markdown.Avalonia.SyntaxHigh; + +// It seems that the Markdown.Avalonia 11.0.0-b1 doesn't have a good support for lua +// syntax highlight, and in version v0.10.14 I can find the ways to custom it +// but there is no way to custom in 11.0.0-b1. +// +// So I write this class as a "plugin" to resolve this problem. +// And this should be followed as below: +// https://github.com/whistyun/Markdown.Avalonia/blob/8aec868dafe7798e0a71667bb4d587cd05760b7a/Markdown.Avalonia.Tight/Markdown.cs#L43-L47 +// https://github.com/whistyun/Markdown.Avalonia/blob/v11.0.0-b1/Markdown.Avalonia.SyntaxHigh/SyntaxSetup.cs +// +// What's more, styles haven't been adapted, which can be referenced from: +// https://github.com/whistyun/Markdown.Avalonia/blob/8aec868dafe7798e0a71667bb4d587cd05760b7a/Markdown.Avalonia.Tight/MarkdownStyle.cs#L26-L30 +// https://github.com/whistyun/Markdown.Avalonia/blob/v11.0.0-b1/Markdown.Avalonia.SyntaxHigh/StyleSetup.cs +// +// And this may be removed if Markdown.Avalonia updates. + +public class SyntaxSetup +{ + public static Func? CreateCodeBlock; + + public IEnumerable>> GetOverrideConverters() + { + yield return new KeyValuePair>( + "CodeBlocksWithLangEvaluator", + CodeBlocksEvaluator); + } + + private Border CodeBlocksEvaluator(Match match) + { + var lang = match.Groups[2].Value; + var code = match.Groups[3].Value; + + if(string.IsNullOrEmpty(lang)) { + var ctxt = new TextBlock() { + Text = code, + TextWrapping = TextWrapping.NoWrap + }; + ctxt.Classes.Add(Markdown.CodeBlockClass); + + var scrl = new ScrollViewer(); + scrl.Classes.Add(Markdown.CodeBlockClass); + scrl.Content = ctxt; + scrl.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + + var result = new Border(); + result.Classes.Add(Markdown.CodeBlockClass); + result.Child = scrl; + + return result; + } else { + // check wheither style is set + //if(!ThemeDetector.IsAvalonEditSetup) { + // SetupStyle(); + //} + + //var txtEdit = new TextEditor(); + //txtEdit.Tag = lang; + + //txtEdit.Text = code; + //txtEdit.HorizontalAlignment = HorizontalAlignment.Stretch; + //txtEdit.IsReadOnly = true; + + var result = new Border(); + result.Classes.Add(Markdown.CodeBlockClass); + if(CreateCodeBlock != null) + result.Child = CreateCodeBlock(lang, code); + + return result; + } + } + +} \ No newline at end of file diff --git a/Mesen.sln b/Mesen.sln index 10ee76d2d..43b2b2a02 100644 --- a/Mesen.sln +++ b/Mesen.sln @@ -41,6 +41,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SevenZip", "SevenZip\SevenZ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Lua", "Lua\Lua.vcxproj", "{B609E0A0-5050-4871-91D6-E760633BCDD1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdown.Avalonia.SyntaxHigh", "Markdown.Avalonia.SyntaxHigh\Markdown.Avalonia.SyntaxHigh.csproj", "{21CD6056-1F8E-414F-A0C6-136998B533BC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -150,6 +152,22 @@ Global {B609E0A0-5050-4871-91D6-E760633BCDD1}.Release|Any CPU.ActiveCfg = Release|x64 {B609E0A0-5050-4871-91D6-E760633BCDD1}.Release|x64.ActiveCfg = Release|x64 {B609E0A0-5050-4871-91D6-E760633BCDD1}.Release|x64.Build.0 = Release|x64 + {21CD6056-1F8E-414F-A0C6-136998B533BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.Debug|x64.ActiveCfg = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.Debug|x64.Build.0 = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.PGO Optimize|Any CPU.ActiveCfg = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.PGO Optimize|Any CPU.Build.0 = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.PGO Optimize|x64.ActiveCfg = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.PGO Optimize|x64.Build.0 = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.PGO Profile|Any CPU.ActiveCfg = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.PGO Profile|Any CPU.Build.0 = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.PGO Profile|x64.ActiveCfg = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.PGO Profile|x64.Build.0 = Debug|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.Release|Any CPU.Build.0 = Release|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.Release|x64.ActiveCfg = Release|Any CPU + {21CD6056-1F8E-414F-A0C6-136998B533BC}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/UI/Debugger/Documentation/meta.lua b/UI/Debugger/Documentation/meta.lua new file mode 100644 index 000000000..70b19caf0 --- /dev/null +++ b/UI/Debugger/Documentation/meta.lua @@ -0,0 +1,641 @@ +-- +-- Annotations of Mesen lua script for LSP(language server protocol). +-- See: https://github.com/LuaLS/lua-language-server/wiki/Annotations +-- You could export this definition and use it for code completion in other +-- code editor with Lua annotation support or Lua LSP. +-- + +--- @meta emu + +--- Used by emu.addMemoryCallback() and emu.removeMemoryCallback() +--- @enum callbackType + +--- Used by emu.addMemoryCallback() and emu.removeMemoryCallback() +--- @class enum.callbackType +--- Callback is called when data is read +--- @field read callbackType +--- Callback is called when data is written +--- @field write callbackType +--- Callback is called when the CPU starts executing an instruction +--- @field exec callbackType + +--- Used by emu.addCheat() +--- @enum cheatType + +--- Used by emu.addCheat() +--- @class enum.cheatType +--- Game Genie (NES) +--- @field nesGameGenie cheatType +--- Pro Action Rocky (NES) +--- @field nesProActionRocky cheatType +--- Custom Address:Value code (NES) +--- @field nesCustom cheatType +--- Game Genie (GB) +--- @field gbGameGenie cheatType +--- Game Shark (GB) +--- @field gbGameShark cheatType +--- Game Genie (SNES) +--- @field snesGameGenie cheatType +--- Pro Action Replay (SNES) +--- @field snesProActionReplay cheatType +--- 24-bit address format (PC Engine) +--- @field pceRaw cheatType +--- 21-bit address format (PC Engine) +--- @field pceAddress cheatType + +--- Used by emu.getAccessCounters() +--- @enum counterType + +--- Used by emu.getAccessCounters() +--- @class enum.counterType +--- Returns the number of times each byte was read +--- @field readCount counterType +--- Returns the number of times each byte was written +--- @field writeCount counterType +--- Returns the number of times each byte was executed +--- @field execCount counterType +--- Returns the time at which each byte was last read +--- @field lastReadClock counterType +--- Returns the time at which each byte was last written +--- @field lastWriteClock counterType +--- Returns the time at which each byte was last executed +--- @field lastExecClock counterType + +--- Used by several APIs to specify which CPU the API call applies to. +--- @enum cpuType + +--- Used by several APIs to specify which CPU the API call applies to. +--- @class enum.cpuType +--- SNES - Main CPU - S-CPU 5A22 (65186) +--- @field snes cpuType +--- SNES - SPC +--- @field spc cpuType +--- SNES - DSP-n +--- @field necDsp cpuType +--- SNES - SA-1 +--- @field sa1 cpuType +--- SNES - CX4 +--- @field cx4 cpuType +--- Game Boy - Main CPU (can also be used in SGB mode) +--- @field gameboy cpuType +--- NES - Main CPU - 2A03 (6502) +--- @field nes cpuType +--- PC Engine - Main CPU - HuC6280 +--- @field pce cpuType + +--- Used by emu.selectDrawSurface() and emu.getDrawSurfaceSize() +--- @enum drawSurface + +--- Used by emu.selectDrawSurface() and emu.getDrawSurfaceSize() +--- @class enum.drawSurface +--- Console's framebuffer. Drawings appear in screenshots/videos. +--- @field consoleScreen drawSurface +--- Separate surface with a configurable resolution. Drawings not shown in screenshots/videos. +--- @field scriptHud drawSurface + +--- Used by emu.addEventCallback() and emu.removeEventCallback() +--- @enum eventType + +--- Used by emu.addEventCallback() and emu.removeEventCallback() +--- @class enum.eventType +--- Triggered when an NMI occurs (not available on some consoles) +--- @field nmi eventType +--- Triggered when an IRQ occurs +--- @field irq eventType +--- Triggered when a frame starts (typically once vertical blank ends) +--- @field startFrame eventType +--- Triggered when a frame ends (typically once vertical blank starts) +--- @field endFrame eventType +--- Triggered when the console is reset (not available on some consoles) +--- @field reset eventType +--- Triggered when the Lua script is stopped +--- @field scriptEnded eventType +--- Triggered after the emulator updates the state of all input devices (once per frame) +--- @field inputPolled eventType +--- Triggered when a savestate is manually loaded +--- @field stateLoaded eventType +--- Triggered when a savestate is manually saved +--- @field stateSaved eventType +--- Triggered when code execution breaks (e.g breakpoint, step, etc.) +--- @field codeBreak eventType + +--- Used by several APIs to specify which memory type to access/use. +--- @enum memType + +--- Used by several APIs to specify which memory type to access/use. +--- @class enum.memType +--- SNES - S-CPU memory +--- @field snesMemory memType +--- SNES - SPC memory +--- @field spcMemory memType +--- SNES - SA-1 memory +--- @field sa1Memory memType +--- SNES - DSP-n memory +--- @field necDspMemory memType +--- SNES - GSU memory +--- @field gsuMemory memType +--- SNES - CX4 memory +--- @field cx4Memory memType +--- Game Boy - CPU memory +--- @field gameboyMemory memType +--- NES - CPU memory +--- @field nesMemory memType +--- NES - PPU memory +--- @field nesPpuMemory memType +--- PC Engine - CPU memory +--- @field pceMemory memType +--- SNES - S-CPU memory (no read/write side-effects) +--- @field snesDebug memType +--- SNES - SPC memory (no read/write side-effects) +--- @field spcDebug memType +--- SNES - SA-1 memory (no read/write side-effects) +--- @field sa1Debug memType +--- SNES - DSP-n memory (no read/write side-effects) +--- @field necDspDebug memType +--- SNES - GSU memory (no read/write side-effects) +--- @field gsuDebug memType +--- SNES - CX4 memory (no read/write side-effects) +--- @field cx4Debug memType +--- Game Boy - CPU memory (no read/write side-effects) +--- @field gameboyDebug memType +--- NES - CPU memory (no read/write side-effects) +--- @field nesDebug memType +--- NES - PPU memory (no read/write side-effects) +--- @field nesPpuDebug memType +--- PC Engine - CPU memory (no read/write side-effects) +--- @field pceDebug memType +--- SNES - PRG ROM +--- @field snesPrgRom memType +--- SNES - Work RAM +--- @field snesWorkRam memType +--- SNES - Save RAM +--- @field snesSaveRam memType +--- SNES - Video RAM +--- @field snesVideoRam memType +--- SNES - Sprite RAM (OAM) +--- @field snesSpriteRam memType +--- SNES - Palette RAM (CGRAM) +--- @field snesCgRam memType +--- SNES - SPC - RAM +--- @field spcRam memType +--- SNES - SPC - IPL ROM +--- @field spcRom memType +--- SNES - DSP-n - Program ROM +--- @field dspProgramRom memType +--- SNES - DSP-n - Data ROM +--- @field dspDataRom memType +--- SNES - DSP-n - Data RAM +--- @field dspDataRam memType +--- SNES - SA-1 - IRAM +--- @field sa1InternalRam memType +--- SNES - GSU - Work RAM +--- @field gsuWorkRam memType +--- SNES - CX4 - Data RAM +--- @field cx4DataRam memType +--- SNES - BS-X - PSRAM +--- @field bsxPsRam memType +--- SNES - BS-X - Memory Pack +--- @field bsxMemoryPack memType +--- Game Boy - PRG ROM +--- @field gbPrgRom memType +--- Game Boy - Work RAM +--- @field gbWorkRam memType +--- Game Boy - Cart/Save RAM +--- @field gbCartRam memType +--- Game Boy - High RAM +--- @field gbHighRam memType +--- Game Boy - Boot ROM +--- @field gbBootRom memType +--- Game Boy - Video RAM +--- @field gbVideoRam memType +--- Game Boy - Sprite RAM +--- @field gbSpriteRam memType +--- NES - PRG ROM +--- @field nesPrgRom memType +--- NES - System RAM +--- @field nesInternalRam memType +--- NES - Work RAM +--- @field nesWorkRam memType +--- NES - Save RAM +--- @field nesSaveRam memType +--- NES - Nametable RAM (CIRAM) +--- @field nesNametableRam memType +--- NES - Sprite RAM (OAM) +--- @field nesSpriteRam memType +--- NES - Secondary Sprite RAM +--- @field nesSecondarySpriteRam memType +--- NES - Palette RAM +--- @field nesPaletteRam memType +--- NES - CHR RAM +--- @field nesChrRam memType +--- NES - CHR ROM +--- @field nesChrRom memType +--- PC Engine - HuCard ROM +--- @field pcePrgRom memType +--- PC Engine - Work RAM +--- @field pceWorkRam memType +--- PC Engine - Save RAM +--- @field pceSaveRam memType +--- PC Engine - CD-ROM Unit RAM +--- @field pceCdromRam memType +--- PC Engine - Card RAM +--- @field pceCardRam memType +--- PC Engine - ADPCM RAM +--- @field pceAdpcmRam memType +--- PC Engine - Arcade Card RAM +--- @field pceArcadeCardRam memType +--- PC Engine - Video RAM (VDC) +--- @field pceVideoRam memType +--- PC Engine - Video RAM (VDC2 - SuperGrafx only) +--- @field pceVideoRamVdc2 memType +--- PC Engine - Sprite RAM (VDC) +--- @field pceSpriteRam memType +--- PC Engine - Sprite RAM (VDC2 - SuperGrafx only) +--- @field pceSpriteRamVdc2 memType +--- PC Engine - Palette RAM (VCE) +--- @field pcePaletteRam memType + +--- Used by emu.step() +--- @enum stepType + +--- Used by emu.step() +--- @class enum.stepType +--- Steps the specified number of instructions +--- @field step stepType +--- Steps out of the current subroutine (not available for all CPUs) +--- @field stepOut stepType +--- Steps over the current subroutine call (not available for all CPUs) +--- @field stepOver stepType +--- Steps the specified number of CPU cycles (not available for all CPUs) +--- @field cpuCycleStep stepType +--- Steps the specified number of scanline cycles +--- @field ppuStep stepType +--- Steps the specified number of scanlines +--- @field ppuScanline stepType +--- Steps the specified number of video frames +--- @field ppuFrame stepType +--- Breaks on the specified scanline number +--- @field specificScanline stepType +--- Breaks on the next NMI event +--- @field runToNmi stepType +--- Breaks on the next IRQ event +--- @field runToIrq stepType + +--- @class Emu +--- @field callbackType enum.callbackType +--- @field cheatType enum.cheatType +--- @field counterType enum.counterType +--- @field cpuType enum.cpuType +--- @field drawSurface enum.drawSurface +--- @field eventType enum.eventType +--- @field memType enum.memType +--- @field stepType enum.stepType +--- lua api entries +emu = {} + +--- Adds the specified cheat code. +--- +--- Note: Cheat codes added via this function are not permanent and not visible in the UI. +--- @param cheatType cheatType Cheat type/format +--- @param cheatCode string Cheat code +function emu.addCheat(cheatType, cheatCode) +end + +--- Registers a callback function to be called whenever the specified event occurs. +--- The callback function receives 1 parameter ("cpuType") which is an enum value (emu.cpuType) which indicates which CPU triggered the event (useful to distinguish between identical events triggered by different CPUs, e.g Super Game Boy). +--- @param callback function Lua function to call when the event occurs +--- @param eventType eventType Event type +--- @return integer Value that can be used to remove the callback by calling emu.removeEventCallback(). +function emu.addEventCallback(callback, eventType) +end + +--- Registers a callback function to be called whenever the specified event occurs. +--- The callback function receives 2 parameters ("address" and "value") that correspond to the address being written to or read from, and the value that is being read/written. +--- +--- For reads, the callback is called after the read is performed. +--- For writes, the callback is called before the write is performed. +--- +--- If the callback returns an integer value, it will replace the value - you can alter the results of read/write operation by using this. +--- @param callback function Lua function to call when the event occurs +--- @param callbackType callbackType Callback type +--- @param startAddress integer Start of the address range +--- @param endAddress integer? End of the address range +--- @param cpuType cpuType? CPU used for the callback +--- @param memoryType memType? Memory type for the callback +--- @return integer Value that can be used to remove the callback by calling emu.removeMemoryCallback(). +function emu.addMemoryCallback(callback, callbackType, startAddress, endAddress, cpuType, memoryType) +end + +--- Breaks the execution. +function emu.breakExecution() +end + +--- Removes all active cheat codes. +--- +--- Note: This has no impact on cheat codes saved in the UI, but it will disable them temporarily. +function emu.clearCheats() +end + +--- Removes all drawn shapes from the screen. +function emu.clearScreen() +end + +--- Converts an address between CPU addressing mode and ROM/RAM addressing mode. +--- When a ROM/RAM address is given, a CPU address matching that value is returned, if the value is mapped. +--- When a CPU address is given, the corresponding ROM/RAM address is returned, if the address is mapped to ROM/RAM. +--- @param address integer Address to convert +--- @param memoryType memType? Memory type +--- @param cpuType cpuType? CPU used for the conversion +--- @return {address: integer, memType: memType} +function emu.convertAddress(address, memoryType, cpuType) +end + +--- Creates a savestate and returns it as a binary string. +--- +--- Note: This can only be called from inside an "exec" memory callback. +--- @return string Binary string containing the savestate. +function emu.createSavestate() +end + +--- Displays a message on the main window in the format "[category] text" +--- @param category string The category is the portion shown between brackets +--- @param text string Text to show on the screen +function emu.displayMessage(category, text) +end + +--- Draws a line between (x, y) to (x2, y2) using the specified color for a specific number of frames. +--- @param x integer X position (start of line) +--- @param y integer Y position (start of line) +--- @param x2 integer X position (end of line) +--- @param y2 integer Y position (end of line) +--- @param color integer? Color +--- @param duration integer? Number of frames to display +--- @param delay integer? Number of frames to wait before drawing the line +function emu.drawLine(x, y, x2, y2, color, duration, delay) +end + +--- Draws a pixel at the specified (x, y) coordinates using the specified color for a specific number of frames. +--- @param x integer X position +--- @param y integer Y position +--- @param color integer Color +--- @param duration integer? Number of frames to display +--- @param delay integer? Number of frames to wait before drawing the pixel +function emu.drawPixel(x, y, color, duration, delay) +end + +--- Draws a rectangle between (x, y) to (x+width, y+height) using the specified color for a specific number of frames. If fill is false, only the rectangle's outline will be drawn. +--- @param x integer X position +--- @param y integer Y position +--- @param width integer Width +--- @param height integer Height +--- @param color integer? Color +--- @param fill boolean? Whether or not to draw an outline, or a filled rectangle. +--- @param duration integer? Number of frames to display +--- @param delay integer? Number of frames to wait before drawing the rectangle +function emu.drawRectangle(x, y, width, height, color, fill, duration, delay) +end + +--- Draws text at (x, y) using the specified text and colors for a specific number of frames. +--- @param x integer X position +--- @param y integer Y position +--- @param text string Text to display +--- @param textColor integer? Color to use for the text +--- @param backgroundColor integer? Color to use for the background +--- @param maxWidth integer? Max width (pixels) - wraps to next line when reached +--- @param duration integer? Number of frames to display +--- @param delay integer? Number of frames to wait before drawing the text +function emu.drawString(x, y, text, textColor, backgroundColor, maxWidth, duration, delay) +end + +--- Returns an array of access counters for the specified memory and operation types. +--- @param counterType counterType Counter type +--- @param memoryType memType Memory type +--- @return integer[] Array of ints +function emu.getAccessCounters(counterType, memoryType) +end + +--- Returns a table containing the full size, visible size and overscan size for the selected draw surface. +--- @param surface drawSurface? Draw surface +--- @return { width: integer, height: integer, visibleWidth: integer, visibleHeight: integer, overscan: {top: integer, bottom: integer, left: integer, right: integer}} +function emu.getDrawSurfaceSize(surface) +end + +--- Returns a table containing the state of all buttons for the selected port/controller. The table's content varies based on the controller type. +--- @param port integer Port number +--- @param subPort integer? Subport number - this is used for multitap-like adapters. +--- @return { ["a"| "b" | "select" | "start" | "up" | "down" | "left" | "right" | string ]: boolean } Content varies based on controller type. +function emu.getInput(port, subPort) +end + +--- Returns a table containing the address and memory type for the specified label. +--- @param label string Label +--- @return { address: integer, memType: memType} . Note: Returns nil when the specified label could not be found. +function emu.getLabelAddress(label) +end + +--- Returns the same text as what is shown in the emulator's log window. +--- @param label string Label +--- @return table A string containing the log shown in the log window +function emu.getLogWindowLog(label) +end + +--- Returns the size (in bytes) of the specified memory type. +--- @param memoryType memType Memory type +--- @return integer Size of the specified memory type +function emu.getMemorySize(memoryType) +end + +--- Returns a table containing the position and the state of all 3 buttons. +--- @return { x : integer, y : integer, relativeX : integer, relativeY : integer, left : boolean, middle : boolean, right : boolean } +function emu.getMouseState() +end + +--- Returns the color (in ARGB format) of the screen's output for the specified coordinates. +--- @param x integer X position +--- @param y integer Y position +--- @return integer ARGB color +function emu.getPixel(x, y) +end + +--- Returns information about the ROM file that is currently running. +--- @return { name : string, path : string, fileSha1Hash : string } +function emu.getRomInfo() +end + +--- Returns an array of ARGB values with the contents of the console's screen - can be used with emu.setScreenBuffer() to modify the screen's contents. +--- +--- Note: The size of the array varies based on the console, game, and sometimes scene. Use emu.getScreenSize() to get the screen's current dimensions. +--- @return integer[] Array of ARGB values +function emu.getScreenBuffer() +end + +--- Returns a table containing the size of the console's current screen output. +--- @return { width : integer, height : integer } +function emu.getScreenSize() +end + +--- This function returns the path to a unique folder (based on the script's filename) where the script should store its data (if any data needs to be saved). The data will be saved in subfolders inside the LuaScriptData folder in Mesen's home folder. +--- +--- Note: This function will return an empty string if the "Allow access to I/O and OS functions" option is disabled. +--- @return string The folder's path +function emu.getScriptDataFolder() +end + +--- Returns a table containing key-value pairs that describe the console's current state. +--- +--- Note: The name of the values returned may change from one version to another. Some values may represent the emulator's internal state and may not be useful (these will be hidden in future versions.) +--- @return table Content varies for each console and game. +function emu.getState() +end + +--- Returns whether or not a specific key is pressed. The "keyName" must be the same as the string shown in the UI when the key is bound to a button. +--- @param keyName string Name of the key to check +--- @return boolean The key's state (true when pressed) +function emu.isKeyPressed(keyName) +end + +--- Loads a savestate from a binary string. +--- +--- Note: This can only be called from inside an "exec" memory callback. +--- @param state string Binary data containing the savestate +function emu.loadSavestate(state) +end + +--- Logs the specified string in the script's log window - useful for debugging scripts. +--- @param text string Text to log +function emu.log(text) +end + +--- Measures the specified string and returns a table containing the width and height that the string would take when drawn. +--- @param text string String to measure +--- @param maxWidth integer? Max width (pixels) - wraps to next line when reached +--- @return { width : integer, height : integer } +function emu.measureString(text, maxWidth) +end + +--- Reads an 8-bit value from the specified address and memory type. +--- +--- Note: When using "memType.[cpuName]" memory types, side-effects can occur from reading a value. Use the "memType.[cpuName]Debug" enum values to avoid side-effects. +--- @param address integer Address to read from +--- @param memoryType memType Memory type to read from +--- @param signed boolean When true, the return value is an 8-bit signed value. +--- @return integer An 8-bit (signed or unsigned) value. +function emu.read(address, memoryType, signed) +end + +--- Reads a 16-bit value from the specified address and memory type. +--- +--- Note: When using "memType.[cpuName]" memory types, side-effects can occur from reading a value. Use the "memType.[cpuName]Debug" enum values to avoid side-effects. +--- @param address integer Address to read from +--- @param memoryType memType Memory type to read from +--- @param signed boolean When true, the return value is a 16-bit signed value. +--- @return integer A 16-bit (signed or unsigned) value. +function emu.readWord(address, memoryType, signed) +end + +--- Resets the current game. If the console does not have a reset button, this will have the same effect as power cycling. +function emu.reset() +end + +--- Resets all access counters. +function emu.resetAccessCounters() +end + +--- Resumes execution after a break. +function emu.resume() +end + +--- Instantly rewinds the emulation by the number of seconds specified. +--- +--- Note: This can only be called from inside an "exec" memory callback. +--- @param seconds integer Number of seconds to rewind +function emu.rewind(seconds) +end + +--- Selects the surface on which any subsequent draw call will be drawn to. +--- +--- consoleScreen: This surface is the same as the console's output and is a fixed resolution. Anything drawn here will appear in screenshots/videos. +--- +--- scriptHud: This surface is independent to the console's output and has a configurable resolution. Drawings done on this surface will not appear in screenshots/videos. +--- +--- Note: setScreenBuffer always draws to the "consoleScreen" surface. +--- @param surface drawSurface Draw surface +--- @param scale integer? Scale to use for the "scriptHud" surface (max: 4) +function emu.selectDrawSurface(surface, scale) +end + +--- Sets the input state for the specified port. Buttons enabled or disabled via setInput will keep their state until the next inputPolled event. +--- +--- Note: If a button's value is not specified in the "input" argument, then the player retains control of that button. For example, "emu.setInput({ select = false, start = false }, 0)" will prevent the player 1 from using both the start and select buttons, but all other buttons will still work as normal. +--- +--- It is recommended to use this function within a callback for the inputPolled event. Otherwise, the inputs may not be applied before the ROM has the chance to read them. +--- @param input table Controller state to apply to the port, same format as the return value of getState(). +--- @param port integer Port number +--- @param subPort integer? Subport number - this is used for multitap-like adapters. +function emu.setInput(input, port, subPort) +end + +--- Replaces the current frame with the contents of the specified array. +--- @param screenBuffer integer[] Array of integers in ARGB format +function emu.setScreenBuffer(screenBuffer) +end + +--- Changes the state of the emulator to match the values provided in the "state" parameter. +--- +--- Note: The name of the values returned may change from one version to another. Some values may represent the emulator's internal state and should not be changed (access to these will be removed in future versions.) +--- @param state table A key-value table containing the state to be applied. +function emu.setState(state) +end + +--- Breaks the emulation's execution when the step conditions are reached. +--- @param count integer Number of cycles/frames/etc. +--- @param stepType stepType Step type +--- @param cpuType cpuType? CPU type +function emu.step(count, stepType, cpuType) +end + +--- Stops the emulation and returns the specified exit code (when used with the --testRunner command line option). +--- @param exitCode integer The exit code that the Mesen process will return. +function emu.stop(exitCode) +end + +--- Takes a screenshot and returns a PNG file as a string. The screenshot is not saved to the disk. +--- @return string A binary string containing a PNG image. +function emu.takeScreenshot() +end + +--- Removes a previously registered callback function. +--- @param reference integer Value returned by the call to emu.addEventCallback() +--- @param eventType eventType Event type +function emu.removeEventCallback(reference, eventType) +end + +--- Removes a previously registered callback function. +--- @param reference integer Value returned by the call to emu.addMemoryCallback() +--- @param callbackType callbackType Callback type +--- @param startAddress integer Start of the address range +--- @param endAddress integer? End of the address range +--- @param cpuType cpuType? CPU used for the callback +--- @param memoryType memType? Memory type +function emu.removeMemoryCallback(reference, callbackType, startAddress, endAddress, cpuType, memoryType) +end + +--- Writes an 8-bit value to the specified address and memory type. +--- +--- Note: When using "memType.[cpuName]" memory types, side-effects can occur from writing a value. Use the "memType.[cpuName]Debug" enum values to avoid side-effects. +--- @param address integer Address to write to +--- @param value integer 8-bit value to write +--- @param memoryType memType Memory type to write to +function emu.write(address, value, memoryType) +end + +--- Writes a 16-bit value to the specified address and memory type. +--- +--- Note: When using "memType.[cpuName]" memory types, side-effects can occur from writing a value. Use the "memType.[cpuName]Debug" enum values to avoid side-effects. +--- @param address integer Address to write to +--- @param value integer 16-bit value to write +--- @param memoryType memType Memory type to write to +function emu.writeWord(address, value, memoryType) +end + +return emu \ No newline at end of file diff --git a/UI/Debugger/ViewModels/ScriptWindowViewModel.cs b/UI/Debugger/ViewModels/ScriptWindowViewModel.cs index 87feab073..e46b172d8 100644 --- a/UI/Debugger/ViewModels/ScriptWindowViewModel.cs +++ b/UI/Debugger/ViewModels/ScriptWindowViewModel.cs @@ -139,7 +139,7 @@ private List GetToolbarActions() ActionType = ActionType.BuiltInScripts, AlwaysShowLabel = true, SubActions = GetBuiltInScriptActions() - }); + }); return actions; } @@ -195,7 +195,7 @@ private void UpdateScriptId(int scriptId) private List GetScriptMenuActions() { - return new() { + return new() { new ContextMenuAction() { ActionType = ActionType.RunScript, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.ScriptWindow_RunScript), @@ -347,5 +347,24 @@ private async Task SaveAs(string newName) } return false; } + + public Uri GetCodeUri() + { + return FilePath.Length > 0 + // TODO handle uri for document + ? new UriBuilder { }.Uri + : new UriBuilder { Scheme = UriSchema, Path = "script.lua" }.Uri; + } + + public Uri GetWorkspaceUri() + { + return new UriBuilder { Scheme = UriSchema, Path = "/" }.Uri; + } + public Uri GetMetaLuaUri() + { + return new UriBuilder { Scheme = UriSchema, Path = "meta.lua" }.Uri; + } + + public static string UriSchema = "MesenLua"; } } diff --git a/UI/Debugger/Views/ScriptCodeHoverView.axaml b/UI/Debugger/Views/ScriptCodeHoverView.axaml new file mode 100644 index 000000000..18fa8cf57 --- /dev/null +++ b/UI/Debugger/Views/ScriptCodeHoverView.axaml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/UI/Debugger/Views/ScriptCodeHoverView.axaml.cs b/UI/Debugger/Views/ScriptCodeHoverView.axaml.cs new file mode 100644 index 000000000..b9ae51ad9 --- /dev/null +++ b/UI/Debugger/Views/ScriptCodeHoverView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Mesen.Debugger.Views +{ + public class ScriptCodeHoverView : UserControl + { + public ScriptCodeHoverView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/UI/Debugger/Windows/ScriptWindow.axaml.cs b/UI/Debugger/Windows/ScriptWindow.axaml.cs index 50e437889..dbff9bd9d 100644 --- a/UI/Debugger/Windows/ScriptWindow.axaml.cs +++ b/UI/Debugger/Windows/ScriptWindow.axaml.cs @@ -1,11 +1,9 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Input; -using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Imaging; -using Avalonia.Styling; using Avalonia.Threading; using AvaloniaEdit; using AvaloniaEdit.CodeCompletion; @@ -13,18 +11,24 @@ using AvaloniaEdit.Editing; using AvaloniaEdit.Highlighting; using AvaloniaEdit.Highlighting.Xshd; -using Mesen.Config; +using DynamicData; using Mesen.Debugger.Controls; -using Mesen.Debugger.Utilities; using Mesen.Debugger.ViewModels; using Mesen.Debugger.Views; using Mesen.Interop; using Mesen.Utilities; +using OmniSharp.Extensions.LanguageServer.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using ReactiveUI; using System; -using System.Collections.Generic; -using System.ComponentModel; +using System.Diagnostics; +using System.IO; using System.Linq; +using System.Reactive.Linq; using System.Reflection; +using System.Threading.Tasks; using System.Xml; namespace Mesen.Debugger.Windows @@ -32,16 +36,28 @@ namespace Mesen.Debugger.Windows public class ScriptWindow : MesenWindow, INotificationHandler { private static XshdSyntaxDefinition _syntaxDef; + private static readonly string _luaMetaDefinition; private IHighlightingDefinition _highlighting; private MesenTextEditor _textEditor; private TextBox _txtScriptLog; private DispatcherTimer _timer; private ScriptWindowViewModel _model; + private Process _lspServer = null!; + private LanguageClient? _lspClient = null!; static ScriptWindow() { using XmlReader reader = XmlReader.Create(Assembly.GetExecutingAssembly().GetManifestResourceStream("Mesen.Debugger.HighlightLua.xshd")!); _syntaxDef = HighlightingLoader.LoadXshd(reader); + + { + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Mesen.Debugger.Documentation.meta.lua")!; + using var metaReader = new StreamReader(stream); + _luaMetaDefinition = metaReader.ReadToEnd(); + } + + // Install syntax for document in completion and hover (here to initialize looks good now). + MarkdownCodeBlockHelper.Install(); } [Obsolete("For designer only")] @@ -62,9 +78,8 @@ public ScriptWindow(ScriptWindowViewModel model) _textEditor = this.GetControl("Editor"); _textEditor.TextArea.KeyDown += TextArea_KeyDown; _textEditor.TextArea.KeyUp += TextArea_KeyUp; - _textEditor.TextArea.TextEntered += TextArea_TextEntered; _textEditor.TextArea.TextEntering += TextArea_TextEntering; - _textEditor.TextArea.TextView.PointerMoved += TextView_PointerMoved; + //_textEditor.TextArea.TextView.PointerMoved += TextView_PointerMoved; _txtScriptLog = this.GetControl("txtScriptLog"); _timer = new DispatcherTimer(TimeSpan.FromMilliseconds(200), DispatcherPriority.Normal, (s, e) => UpdateLog()); @@ -77,6 +92,84 @@ public ScriptWindow(ScriptWindowViewModel model) _model.Config.LoadWindowSettings(this); _textEditor.SyntaxHighlighting = _highlighting; + + #region Start Lsp + + var _ = InitializeLspClientAsync(); + + #endregion + } + + private async Task InitializeLspClientAsync() + { + _lspServer = new Process(); + _lspServer.StartInfo.FileName = LspServerHelper.ExecutableFullName; + _lspServer.StartInfo.RedirectStandardInput = true; + _lspServer.StartInfo.RedirectStandardOutput = true; + _lspServer.StartInfo.CreateNoWindow = true; + try { + _lspServer.Start(); + } catch(Exception) { + return; + } + + _lspClient = LanguageClient.Create(options => + options + .WithOutput(_lspServer.StandardInput.BaseStream) + .WithInput(_lspServer.StandardOutput.BaseStream) + .WithClientCapabilities(new() { + TextDocument = new() { + Completion = new CompletionCapability { + CompletionItem = new() { + InsertReplaceSupport = true, + } + } + } + }) + ); + + // TODO handle for initializing lsp client + await _lspClient.Initialize(default); + _lspClient.DidOpenTextDocument(new() { + TextDocument = new() { + LanguageId = "lua", + Text = _luaMetaDefinition, + Uri = _model.GetMetaLuaUri(), + } + }); + + // mouse hover tips + Observable + .FromEventPattern( + _textEditor.TextArea.TextView, + nameof(_textEditor.TextArea.TextView.PointerMoved)) + .ObserveOn(RxApp.MainThreadScheduler) + .Select(p => _textEditor.TextArea.TextView.GetPosition(p.EventArgs.GetCurrentPoint(_textEditor.TextArea.TextView).Position + _textEditor.TextArea.TextView.ScrollOffset)) + .Where(p => p != null) + .Select(p => (TextViewPosition)p!) + .Distinct(p => p.Location) //! i don't know why this event is keeping emitted even when I don't move my mouse + .Throttle(TimeSpan.FromSeconds(1)) + .SubscribeOn(RxApp.TaskpoolScheduler) + .Subscribe(pos => HandleEditorHover(pos.Location.Line - 1, pos.Location.Column - 1)); + + // tips on fly + Observable + .FromEventPattern( + _textEditor.TextArea, + nameof(_textEditor.TextArea.TextEntered)) + .ObserveOn(RxApp.MainThreadScheduler) + .SubscribeOn(RxApp.TaskpoolScheduler) + .Where(x => + !string.IsNullOrWhiteSpace(x.EventArgs.Text) + && !x.EventArgs.Text.EndsWith(")")) + .Select(x => new { + Raw = x, + _textEditor.TextArea.Caret.Position, + }) + .Throttle(TimeSpan.FromSeconds(0.5)) + // if caret moved after input, don't show completion + .Where(x => x.Position.Equals(_textEditor.TextArea.Caret.Position)) + .Subscribe(_ => ShowCompletions()); } protected override void OnOpened(EventArgs e) @@ -107,6 +200,7 @@ protected override void OnClosing(WindowClosingEventArgs e) } else { _model.StopScript(); _timer.Stop(); + _lspServer.Kill(); _model.Config.SaveWindowSettings(this); } } @@ -118,7 +212,7 @@ private async void ValidateExit() Close(); } } - + private void UpdateSyntaxDef() { Color[] colors = new Color[] { Colors.Green, Colors.SteelBlue, Colors.Blue, Colors.DarkMagenta, Colors.DarkRed, Colors.Black, Colors.Indigo }; @@ -158,68 +252,47 @@ public void ProcessNotification(NotificationEventArgs e) private CompletionWindow? _completionWindow; private bool _ctrlPressed; - private DocEntryViewModel? _prevTooltipEntry; + private Hover? _previousHover; - private void TextView_PointerMoved(object? sender, PointerEventArgs e) + private Uri OpenCodeForLsp() { - TextViewPosition? posResult = _textEditor.TextArea.TextView.GetPosition(e.GetCurrentPoint(_textEditor.TextArea.TextView).Position + _textEditor.TextArea.TextView.ScrollOffset); - if(posResult is TextViewPosition pos) { - int offset = _textEditor.TextArea.Document.GetOffset(pos.Location.Line, pos.Location.Column); - DocEntryViewModel? entry = GetTooltipEntry(offset); - if(_prevTooltipEntry != entry) { - if(entry != null) { - TooltipHelper.ShowTooltip(_textEditor.TextArea.TextView, new ScriptCodeCompletionView() { DataContext = entry }, 10); - } else { - TooltipHelper.HideTooltip(_textEditor.TextArea.TextView); - } - _prevTooltipEntry = entry; - } - } + // TODO handle open/change/save/... for editing code + var uri = _model.GetCodeUri(); + _lspClient?.DidOpenTextDocument(new() { + TextDocument = new() { Uri = uri, Text = _model.Code }, + }); + return uri; } - private DocEntryViewModel? GetTooltipEntry(int offset) + private void HandleEditorHover(int line, int column) { - if(offset >= _model.Code.Length || _model.Code[offset] == '\r' || _model.Code[offset] == '\n') { - //End of line/document, close tooltip - return null; - } - - //Find the end of the expression - for(; offset < _model.Code.Length; offset++) { - if(!char.IsLetterOrDigit(_model.Code[offset]) && _model.Code[offset] != '.' && _model.Code[offset] != '_') { - break; - } - } - - //Find the start of the expression - int i = offset - 1; - for(; i >= 0 && i < _model.Code.Length; i--) { - if(!char.IsLetterOrDigit(_model.Code[i]) && _model.Code[i] != '.' && _model.Code[i] != '_') { - break; - } - } - - string expr = _model.Code.Substring(i + 1, offset - i - 1); - if(expr.StartsWith("emu.")) { - expr = expr.Substring(4); - bool hasTrailingDot = expr.EndsWith("."); - if(hasTrailingDot) { - expr = expr.Substring(0, expr.Length - 1); - } - - DocEntryViewModel? entry = null; - if(expr.Contains(".")) { - string[] parts = expr.Split('.'); - entry = CodeCompletionHelper.GetEntry(parts[0]); - if(parts.Length == 2 && entry != null && entry.EnumValues.Count > 0) { - return entry; - } + _ = Task.Run(async () => { + if(_lspClient == null) return; + + var hoverResult = await _lspClient.RequestHover(new() { + TextDocument = new() { Uri = OpenCodeForLsp() }, + Position = new(line, column) + }); + + if(_previousHover == null + // TODO identify whether updated + || !string.Equals( + _previousHover.Contents.MarkupContent?.ToString(), + hoverResult?.Contents.MarkupContent?.ToString()) + ) { + Dispatcher.UIThread.Post(() => { + if(hoverResult != null) { + TooltipHelper.ShowTooltip( + _textEditor.TextArea.TextView, + new ScriptCodeHoverView() { DataContext = hoverResult.Contents.MarkupContent }, + 10); + } else { + TooltipHelper.HideTooltip(_textEditor.TextArea.TextView); + } + _previousHover = hoverResult; + }); } - - return CodeCompletionHelper.GetEntry(expr); - } - - return null; + }); } private void TextArea_KeyUp(object? sender, KeyEventArgs e) @@ -233,6 +306,8 @@ private void TextArea_KeyDown(object? sender, KeyEventArgs e) { if(e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl || e.KeyModifiers.HasFlag(KeyModifiers.Control)) { _ctrlPressed = true; + } else if(e.Key == Key.Escape) { + TooltipHelper.HideTooltip(_textEditor.TextArea.TextView); } } @@ -250,136 +325,113 @@ private void TextArea_TextEntering(object? sender, TextInputEventArgs e) //Don't type the space if pressing ctrl+space e.Handled = true; - OpenCompletionWindow(); + ShowCompletions(); } // Do not set e.Handled=true. // We still want to insert the character that was typed. } - private void TextArea_TextEntered(object? sender, TextInputEventArgs e) + private void ShowCompletions() { - if(e.Text == ".") { - OpenCompletionWindow(); - } + _ = Task.Run(async () => { + if(_lspClient == null) return; + + var completions = await _lspClient.RequestCompletion(new() { + TextDocument = new() { Uri = OpenCodeForLsp() }, + Position = new(_textEditor.TextArea.Caret.Line - 1, _textEditor.TextArea.Caret.Column - 1) + }); + if(completions is null) return; + + Dispatcher.UIThread.Post(() => { + _completionWindow = new CompletionWindow(_textEditor.TextArea); + _completionWindow + .CompletionList + .CompletionData + .AddRange(completions.Select(x => new LspCompletionData(x, async () => await _lspClient.ResolveCompletion(x)))); + _completionWindow.Closed += (sender, e) => _completionWindow = null; + _completionWindow.Show(); + }); + }); } - private void OpenCompletionWindow() + public class LspCompletionData : ICompletionData { - int offset = _textEditor.TextArea.Caret.Offset; - //Find the start of the expression - int i = offset - 1; - for(; i >= 0 && i < _model.Code.Length; i--) { - if(!char.IsLetterOrDigit(_model.Code[i]) && _model.Code[i] != '.' && _model.Code[i] != '_') { - break; - } - } - - string expr = _model.Code.Substring(i + 1, offset - i - 1); - if(expr.StartsWith("emu.")) { - expr = expr.Substring(4); - bool hasTrailingDot = expr.EndsWith("."); - if(hasTrailingDot) { - expr = expr.Substring(0, expr.Length - 1); - } - - DocEntryViewModel? entry = null; - if(expr.Contains(".")) { - string[] parts = expr.Split('.'); - entry = CodeCompletionHelper.GetEntry(parts[0]); - if(parts.Length == 2 && entry != null && entry.EnumValues.Count > 0) { - OpenCompletionWindow(entry.EnumValues.Select(x => x.Name), expr, parts[1], parts[1].Length); - return; - } - } - - entry = CodeCompletionHelper.GetEntry(expr); - if(entry != null && entry.EnumValues.Count > 0 && hasTrailingDot) { - OpenCompletionWindow(entry.EnumValues.Select(x => x.Name), expr, "", 0); - } else { - if(!hasTrailingDot) { - OpenCompletionWindow(CodeCompletionHelper.GetEntries(), null, expr, expr.Length); + private readonly CompletionItem _completionItem; + private CompletionItem? _completionDetailed = null; + private Func _detailsResolver; + private CompletionItem CompletionDetailed + { + get + { + if(_completionDetailed == null) { + _completionDetailed = _detailsResolver() ?? _completionItem; } + return _completionDetailed; } } - } - - private void OpenCompletionWindow(IEnumerable entries, string? enumName, string defaultFilter, int insertOffset) - { - _completionWindow = new CompletionWindow(_textEditor.TextArea); - IList data = _completionWindow.CompletionList.CompletionData; - foreach(string name in entries) { - data.Add(new MyCompletionData(name, enumName, -insertOffset)); - } - _completionWindow.Closed += delegate { - _completionWindow = null; - }; - _completionWindow.Show(); - if(defaultFilter.Length > 0) { - _completionWindow.CompletionList.SelectItem(defaultFilter); - } - } - - public class MyCompletionData : ICompletionData - { - private string? _enumName; - private int _insertOffset; - public MyCompletionData(string text, string? enumName = null, int insertOffset = 0) + public LspCompletionData(CompletionItem completionItem, Func> detailsResolver) { - Text = text; - _enumName = enumName; - _insertOffset = insertOffset; + _completionItem = completionItem; + _detailsResolver = () => { + // TODO clean up code + var task = Task.Run(async () => await detailsResolver()); + task.Wait(); + return task.Result; + }; } - public IBitmap Image + // TODO handle image for kinds except enum and function + public IBitmap Image => ImageUtilities.BitmapFromAsset(_completionItem.Kind == CompletionItemKind.Function ? "Assets/Function.png" : "Assets/Enum.png")!; + public string Text => _completionItem.Label; + public object Content => new TextBlock { Text = _completionItem.Label }; + public object Description => new ScriptCodeHoverView { + DataContext = CompletionDetailed.Documentation?.MarkupContent, + MaxHeight = 800, + MaxWidth = 400 + }; + public double Priority { get { - if(_enumName != null) { - return ImageUtilities.BitmapFromAsset("Assets/Enum.png")!; - } else { - return ImageUtilities.BitmapFromAsset(CodeCompletionHelper.GetEntry(Text)?.EnumValues.Count > 0 ? "Assets/Enum.png" : "Assets/Function.png")!; - } - } - } + if(double.TryParse(_completionItem.SortText, out var priority)) + return priority; - public string Text { get; private set; } - - public object Content - { - get { return new TextBlock() { Text = this.Text }; } + return 0; + } } - public object Description + public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs) { - get - { - if(_enumName != null) { - DocEntryViewModel? enumEntry = CodeCompletionHelper.GetEntry(_enumName); - if(enumEntry != null) { - foreach(DocEnumValue val in enumEntry.EnumValues) { - if(val.Name == Text) { - return val.Description; - } - } - } - } else { - DocEntryViewModel? entry = CodeCompletionHelper.GetEntry(Text); - if(entry != null) { - return new ScriptCodeCompletionView() { DataContext = entry }; + // TODO CompletionDetailed.AdditionalTextEdits; CompletionDetailed.TextEdit; may be better. + // However LSP server doesn't response with these fields and I don't know reason yet. + // See `textEdit?: TextEdit | InsertReplaceEdit;` from https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItem + // May be helpful. + + var toInsert = CompletionDetailed.InsertText ?? _completionItem.Label; + var caretPosition = textArea.Caret.Position; + int offset = completionSegment.Offset; + int length = completionSegment.Length; + if(completionSegment.Length == 0) { + length = 1; + + // find the real offset and length to replace existing partial code. + int newLength = 0; + for(; completionSegment.Offset - length > textArea.Document.GetOffset(caretPosition.Line, 1); length++) { + if(toInsert.StartsWith(textArea.Document.GetText(completionSegment.Offset - length, length))) { + // match as long as much + newLength = Math.Max(newLength, length); } } - return null!; - } - } - public double Priority => 1.0; + length = newLength; + offset = completionSegment.Offset - length; + } - public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs) - { - textArea.Document.Replace(completionSegment.Offset + _insertOffset, completionSegment.Length - _insertOffset, this.Text); + textArea.Document.Replace(offset, length, toInsert); } } + } } diff --git a/UI/MarkdownCodeBlockHelper.cs b/UI/MarkdownCodeBlockHelper.cs new file mode 100644 index 000000000..59f0f4b16 --- /dev/null +++ b/UI/MarkdownCodeBlockHelper.cs @@ -0,0 +1,66 @@ +using Avalonia.Controls; +using AvaloniaEdit; +using AvaloniaEdit.Highlighting; +using AvaloniaEdit.Highlighting.Xshd; +using AvaloniaEdit.TextMate; +using Markdown.Avalonia.SyntaxHigh; +using System.Collections.Generic; +using System.Reflection; +using System.Xml; +using TextMateSharp.Grammars; + +namespace Mesen; + +public class MarkdownCodeBlockHelper +{ + private static XshdSyntaxDefinition _syntaxDef = null!; + private static IHighlightingDefinition _highlighting = null!; + private static Dictionary cache = new Dictionary(); + private static RegistryOptions options = new RegistryOptions(ThemeName.LightPlus); + public static void Install() + { + using XmlReader reader = XmlReader.Create(Assembly.GetExecutingAssembly().GetManifestResourceStream("Mesen.Debugger.HighlightLua.xshd")!); + _syntaxDef = HighlightingLoader.LoadXshd(reader); + _highlighting = HighlightingLoader.Load(_syntaxDef, HighlightingManager.Instance); + + SyntaxSetup.CreateCodeBlock = CreateCodeBlock; + } + + private static Control CreateCodeBlock(string lang, string code) + { + var textEditor = new TextEditor() { + Tag = lang, + Text = code, + IsReadOnly = true, + }; + + if("lua".Equals(lang)) { + // + // It seems that TextMate renders slowly. + // Syntax definition may duplicate with `ScriptWindow`. + // And this way works fine. + // + // It's suggested to write another syntax definition for this. + // Because some of this code doesn't belong to lua. + // Such as type annotation for parameter `i` in following: + // ```lua + // function foo(i: integer) + // end + // ``` + // + // I think all of these work is adequate for a simple code editing. + // + textEditor.SyntaxHighlighting = _highlighting; + } else { + var installation = textEditor.InstallTextMate(options); + var ok = cache.TryGetValue(lang, out var language); + if(!ok) { + language = options.GetLanguageByExtension("." + lang); + cache[lang] = language; + } + installation.SetGrammar(options.GetScopeByLanguageId(language?.Id)); + } + + return textEditor; + } +} diff --git a/UI/Program.cs b/UI/Program.cs index 7733667a4..42a132884 100644 --- a/UI/Program.cs +++ b/UI/Program.cs @@ -14,6 +14,11 @@ using System.Diagnostics; using System.Runtime.InteropServices; using Mesen.Interop; +using System.Net; +using System.Net.Http; +using SharpCompress.Writers; +using SharpCompress.Common; +using SharpCompress.Readers; namespace Mesen { @@ -66,6 +71,7 @@ public static int Main(string[] args) //Extract core dll & other native dependencies ExtractNativeDependencies(ConfigManager.HomeFolder); + Task.Run(async () => await DownloadLuaLsp(Path.Combine(ConfigManager.HomeFolder, LspServerHelper.LspDirectoryName))); if(CommandLineHelper.IsTestRunner(args)) { return TestRunner.Run(args); @@ -83,6 +89,45 @@ public static int Main(string[] args) return 0; } + public static async Task DownloadLuaLsp(string dest) + { + // When `HomeFolder/LuaLSP/bin/lua-language-server[.exe]` doesn't exist, then + // AUTO Download latest lua lsp from https://github.com/LuaLS/lua-language-server/releases + + if(File.Exists(Path.Combine(dest, "bin", LspServerHelper.ExecutableName))) + return; + + // TODO test on different systems and processor + // Windows X64: OK! + + Directory.CreateDirectory(dest); + + var downloadUrl = await LspServerHelper.GetDownloadUrl(); + if(downloadUrl == null) return; + + var fileName = Path.Combine(dest, Path.GetFileName(downloadUrl)); + if(!File.Exists(fileName)) { + // Download only when archive file doesn't exist + // This could allow user to download by themselves. + using var ns = await new HttpClient().GetStreamAsync(downloadUrl); + using var fs = File.OpenWrite(fileName); + await ns.CopyToAsync(fs); + } + + using var archiveStream = File.OpenRead(fileName); + if(fileName.EndsWith("tar.gz")) { + using var tar = ReaderFactory.Open(archiveStream); + tar.WriteAllToDirectory(dest, new() { + ExtractFullPath = true + }); + } else if(fileName.EndsWith("zip")) { + using var zip = new ZipArchive(archiveStream); + zip.ExtractToDirectory(dest); + } else { + // unreachable + } + } + public static void ExtractNativeDependencies(string dest) { using(Stream? depStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Mesen.Dependencies.zip")) { diff --git a/UI/UI.csproj b/UI/UI.csproj index dc660ffa4..a131d90f0 100644 --- a/UI/UI.csproj +++ b/UI/UI.csproj @@ -50,6 +50,7 @@ + @@ -88,12 +89,17 @@ + + + + + @@ -556,6 +562,11 @@ + + + + + diff --git a/UI/Utilities/LspServerHelper.cs b/UI/Utilities/LspServerHelper.cs new file mode 100644 index 000000000..8cdaa61fe --- /dev/null +++ b/UI/Utilities/LspServerHelper.cs @@ -0,0 +1,50 @@ +using Mesen.Config; +using Octokit; +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Mesen.Utilities; + +public static class LspServerHelper +{ + public const string LspDirectoryName = "LuaLSP"; + public static string ExecutableName => OperatingSystem.IsWindows() ? "lua-language-server.exe" : "lua-language-server"; + public static string ExecutableFullName => Path.Combine(ConfigManager.HomeFolder, LspDirectoryName, "bin", ExecutableName); + + public static async Task GetDownloadUrl() + { + var system = OperatingSystem.IsWindows() + ? "win32" + : OperatingSystem.IsLinux() + ? "linux" + : OperatingSystem.IsMacOS() + ? "darwin" + : "?"; + + string arch = "?"; + switch(RuntimeInformation.ProcessArchitecture) { + case Architecture.Arm64: + arch = "arm64"; + break; + case Architecture.X64: + arch = "x64"; + break; + case Architecture.X86: + arch = "ia32"; + break; + } + + if(arch.Equals("?") || system.Equals("?")) return null; + + string ext = OperatingSystem.IsWindows() ? "zip" : "tar.gz"; + + var client = new GitHubClient(new ProductHeaderValue("mesen2")); + var releases = await client.Repository.Release.GetAll("LuaLS", "lua-language-server"); + var downloadUrl = releases.FirstOrDefault()?.Assets.FirstOrDefault(x => x.BrowserDownloadUrl.EndsWith($"{system}-{arch}.{ext}"))?.BrowserDownloadUrl; + + return downloadUrl; + } +}