diff --git a/.github/workflows/build-matrix.yml b/.github/workflows/build-matrix.yml index e733a03c4..b93a4208a 100644 --- a/.github/workflows/build-matrix.yml +++ b/.github/workflows/build-matrix.yml @@ -55,6 +55,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Download LuaLS binaries + run: node build-scripts/download-luals.js + - name: Update package.json if: ${{ inputs.versionOverride != '' }} run: npm version ${{ inputs.versionOverride }} --no-git-tag-version diff --git a/.gitignore b/.gitignore index ee77288e2..83f325163 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ buildVariables.json screenshot* src/electron/main.ts .vscode/settings.json +build-assets/lua-language-server* diff --git a/build-assets/lua-annotations/grid-api.lua b/build-assets/lua-annotations/grid-api.lua new file mode 100644 index 000000000..dde7e8382 --- /dev/null +++ b/build-assets/lua-annotations/grid-api.lua @@ -0,0 +1,861 @@ +---@meta +-- ============================================================================= +-- Grid Lua API Annotations +-- +-- This file provides type information and documentation for the Grid Lua API +-- so that lua-language-server (LuaLS) can offer completions, hover docs, +-- signature help, and diagnostics in the Monaco editor. +-- +-- Element subclasses are used to scope completions per hardware element type. +-- `self` and `element[]` are NOT declared here — they are injected per-editor +-- via a context document typed to the specific element subclass. +-- ============================================================================= + +-- ============================================================================= +-- Element (base class) +-- +-- Common methods available on all element types. +-- Do not use this type directly in user code — use the specific subclass. +-- ============================================================================= + +---@class Element +---Called after element initialization. Triggers the init event handler. +---@field post_init_cb? fun(self: Element) +---Called when a MIDI message is received from the grid. header = {instr, sx, sy}, data = {channel, command, param1, param2}. +---@field midirx_cb? fun(self: Element, header: integer[], data: integer[]) +---Called when a SysEx message is received from the grid. header = {instr, sx, sy}, data = hex string. +---@field sysexrx_cb? fun(self: Element, header: integer[], data: string) +---Called when an event view message is received (LCD). header = {instr, sx, sy}, event = {page, element, type}, value = {val, min, max}. +---@field eventrx_cb? fun(self: Element, header: integer[], event: integer[], value: integer[], name: string) +local Element = {} + +---Returns (or sets) the 0-based index of this element on the module. +---@param value? integer If provided, sets the index +---@return integer index The element index (0–15 for a 16-element module) +function Element:element_index(value) end + +---Returns (or sets) the LED index for this element. +---@param value? integer If provided, sets the LED index +---@return integer index The LED index +function Element:led_index(value) end + +---Gets or sets the name of this element. +---@param name? string If provided, sets the name +---@return string name Element name +function Element:element_name(name) end + +---Starts a periodic timer for this element. +---@param period integer Timer period in milliseconds +function Element:timer_start(period) end + +---Stops the timer for this element. +function Element:timer_stop() end + +---Triggers an event on this element. +---@param event_type integer Event type index +function Element:event_trigger(event_type) end + +---Sets the LED color for this element. +---@param layer integer LED layer index (-1 for all layers) +---@param colors table Color data table +function Element:led_color(layer, colors) end + +---Sends a MIDI message from this element. +---Pass -1 for any parameter to use the auto-configured value. +---@param channel MIDI_Channel MIDI channel (0–15, or -1 for auto) +---@param command MIDI_Command MIDI command (e.g. 144=NoteOn, 176=CC, or -1 for auto) +---@param param1 MIDI_Param First parameter (0–127, or -1 for auto) +---@param param2 MIDI_Param Second parameter (0–127, or -1 for auto) +function Element:midi_send(channel, command, param1, param2) end + +---Sends a MIDI SysEx message from this element. +---@param ... integer SysEx data bytes +function Element:midi_sysex_send(...) end + +-- ============================================================================= +-- ButtonElement +-- Available on: BU16 +-- ============================================================================= + +---@class ButtonElement : Element +local ButtonElement = {} + +---Returns (or sets) the current button value. +---@param value? integer If provided, sets the button value +---@return integer value Current button value +function ButtonElement:button_value(value) end + +---Returns (or sets) the minimum button value. +---@param value? integer If provided, sets the minimum +---@return integer min Minimum value +function ButtonElement:button_min(value) end + +---Returns (or sets) the maximum button value. +---@param value? integer If provided, sets the maximum +---@return integer max Maximum value +function ButtonElement:button_max(value) end + +---Returns (or sets) the button mode. 0 = momentary. +---@param value? integer If provided, sets the mode +---@return integer mode Button mode +function ButtonElement:button_mode(value) end + +---Returns the button state. 0 = released, 127 = pressed. +---@return integer state Button state (0 or 127) +function ButtonElement:button_state() end + +---Returns the time elapsed since the last button event (milliseconds). +---@return integer ms Elapsed time in milliseconds +function ButtonElement:button_elapsed_time() end + +---Calculates the button step based on mode, min, max, and value. +---Returns false if button mode is 0 (momentary), otherwise returns the current step number. +---@return integer|boolean step Current step, or false if mode is 0 +function ButtonElement:button_step() end + +-- ============================================================================= +-- EncoderElement +-- Available on: EN16 +-- Encoders have both a rotary encoder and a push button. +-- ============================================================================= + +---@class EncoderElement : Element +local EncoderElement = {} + +---Returns (or sets) the current encoder value. +---@param value? integer If provided, sets the encoder value +---@return integer value Current encoder value +function EncoderElement:encoder_value(value) end + +---Returns (or sets) the minimum encoder value. +---@param value? integer If provided, sets the minimum +---@return integer min Minimum value +function EncoderElement:encoder_min(value) end + +---Returns (or sets) the maximum encoder value. +---@param value? integer If provided, sets the maximum +---@return integer max Maximum value +function EncoderElement:encoder_max(value) end + +---Returns (or sets) the encoder mode. +---@param value? integer If provided, sets the mode +---@return integer mode Encoder mode +function EncoderElement:encoder_mode(value) end + +---Returns the encoder state (rotation direction). Values <64 = left, >63 = right. +---@return integer state Encoder state +function EncoderElement:encoder_state() end + +---Returns the encoder velocity. +---@return integer velocity Rotation velocity +function EncoderElement:encoder_velocity() end + +---Returns (or sets) the encoder sensitivity. +---@param value? integer If provided, sets the sensitivity +---@return integer sensitivity Encoder sensitivity +function EncoderElement:encoder_sensitivity(value) end + +---Returns the time elapsed since the last encoder event (milliseconds). +---@return integer ms Elapsed time in milliseconds +function EncoderElement:encoder_elapsed_time() end + +---Returns (or sets) the current button value. +---@param value? integer If provided, sets the button value +---@return integer value Current button value +function EncoderElement:button_value(value) end + +---Returns (or sets) the minimum button value. +---@param value? integer If provided, sets the minimum +---@return integer min Minimum value +function EncoderElement:button_min(value) end + +---Returns (or sets) the maximum button value. +---@param value? integer If provided, sets the maximum +---@return integer max Maximum value +function EncoderElement:button_max(value) end + +---Returns (or sets) the button mode. 0 = momentary. +---@param value? integer If provided, sets the mode +---@return integer mode Button mode +function EncoderElement:button_mode(value) end + +---Returns the button state. 0 = released, 127 = pressed. +---@return integer state Button state (0 or 127) +function EncoderElement:button_state() end + +---Returns the time elapsed since the last button event (milliseconds). +---@return integer ms Elapsed time in milliseconds +function EncoderElement:button_elapsed_time() end + +---Calculates the button step based on mode, min, max, and value. +---@return integer|boolean step Current step, or false if mode is 0 +function EncoderElement:button_step() end + +-- ============================================================================= +-- PotmeterElement +-- Available on: PO16 +-- ============================================================================= + +---@class PotmeterElement : Element +local PotmeterElement = {} + +---Returns (or sets) the current potentiometer value. +---@param value? integer If provided, sets the value +---@return integer value Current potentiometer value +function PotmeterElement:potmeter_value(value) end + +---Returns (or sets) the minimum potentiometer value. +---@param value? integer If provided, sets the minimum +---@return integer min Minimum value +function PotmeterElement:potmeter_min(value) end + +---Returns (or sets) the maximum potentiometer value. +---@param value? integer If provided, sets the maximum +---@return integer max Maximum value +function PotmeterElement:potmeter_max(value) end + +---Returns (or sets) the potentiometer resolution. +---@param value? integer If provided, sets the resolution +---@return integer resolution Potentiometer resolution +function PotmeterElement:potmeter_resolution(value) end + +---Returns the potentiometer state. +---@return integer state Current state +function PotmeterElement:potmeter_state() end + +---Returns the time elapsed since the last potentiometer event (milliseconds). +---@return integer ms Elapsed time in milliseconds +function PotmeterElement:potmeter_elapsed_time() end + +-- ============================================================================= +-- FaderElement +-- Available on: fader modules +-- Faders use the same API as potmeters. +-- ============================================================================= + +---@class FaderElement : Element +local FaderElement = {} + +---Returns (or sets) the current fader value. +---@param value? integer If provided, sets the value +---@return integer value Current fader value +function FaderElement:potmeter_value(value) end + +---Returns (or sets) the minimum fader value. +---@param value? integer If provided, sets the minimum +---@return integer min Minimum value +function FaderElement:potmeter_min(value) end + +---Returns (or sets) the maximum fader value. +---@param value? integer If provided, sets the maximum +---@return integer max Maximum value +function FaderElement:potmeter_max(value) end + +---Returns (or sets) the fader resolution. +---@param value? integer If provided, sets the resolution +---@return integer resolution Fader resolution +function FaderElement:potmeter_resolution(value) end + +---Returns the fader state. +---@return integer state Current state +function FaderElement:potmeter_state() end + +---Returns the time elapsed since the last fader event (milliseconds). +---@return integer ms Elapsed time in milliseconds +function FaderElement:potmeter_elapsed_time() end + +-- ============================================================================= +-- EndlessElement +-- Available on: EF44 +-- Endless encoders have a rotary encoder, a push button, and an LED ring. +-- ============================================================================= + +---@class EndlessElement : Element +local EndlessElement = {} + +---Returns (or sets) the current endless potentiometer value. +---@param value? integer If provided, sets the value +---@return integer value Current value +function EndlessElement:endless_value(value) end + +---Returns (or sets) the minimum endless potentiometer value. +---@param value? integer If provided, sets the minimum +---@return integer min Minimum value +function EndlessElement:endless_min(value) end + +---Returns (or sets) the maximum endless potentiometer value. +---@param value? integer If provided, sets the maximum +---@return integer max Maximum value +function EndlessElement:endless_max(value) end + +---Returns (or sets) the endless potentiometer mode. +---@param value? integer If provided, sets the mode +---@return integer mode Mode value +function EndlessElement:endless_mode(value) end + +---Returns the endless potentiometer state. +---@return integer state Current state +function EndlessElement:endless_state() end + +---Returns the endless potentiometer velocity. +---@return integer velocity Rotation velocity +function EndlessElement:endless_velocity() end + +---Returns the endless potentiometer direction. +---@return integer direction Rotation direction +function EndlessElement:endless_direction() end + +---Returns (or sets) the endless potentiometer sensitivity. +---@param value? integer If provided, sets the sensitivity +---@return integer sensitivity Sensitivity value +function EndlessElement:endless_sensitivity(value) end + +---Returns the LED offset for this endless element. +---@param value? integer If provided, sets the offset +---@return integer offset LED offset +function EndlessElement:led_offset(value) end + +---Returns (or sets) the current button value. +---@param value? integer If provided, sets the button value +---@return integer value Current button value +function EndlessElement:button_value(value) end + +---Returns (or sets) the minimum button value. +---@param value? integer If provided, sets the minimum +---@return integer min Minimum value +function EndlessElement:button_min(value) end + +---Returns (or sets) the maximum button value. +---@param value? integer If provided, sets the maximum +---@return integer max Maximum value +function EndlessElement:button_max(value) end + +---Returns (or sets) the button mode. 0 = momentary. +---@param value? integer If provided, sets the mode +---@return integer mode Button mode +function EndlessElement:button_mode(value) end + +---Returns the button state. 0 = released, 127 = pressed. +---@return integer state Button state (0 or 127) +function EndlessElement:button_state() end + +---Returns the time elapsed since the last button event (milliseconds). +---@return integer ms Elapsed time in milliseconds +function EndlessElement:button_elapsed_time() end + +---Calculates the button step based on mode, min, max, and value. +---@return integer|boolean step Current step, or false if mode is 0 +function EndlessElement:button_step() end + +-- ============================================================================= +-- LCDElement +-- Available on: VSN1 (screen elements) +-- All draw methods operate on a background buffer — call draw_swap() to push +-- changes to the visible screen. +-- ============================================================================= + +---@class LCDElement : Element +local LCDElement = {} + +---Updates the screen with the contents of the background buffer. +function LCDElement:draw_swap() end + +---Draws a pixel at (x, y) with the specified color. +---@param x integer X coordinate +---@param y integer Y coordinate +---@param color integer[] RGB color as {r, g, b} with 8-bit channels (0–255) +function LCDElement:draw_pixel(x, y, color) end + +---Draws a line between two points. +---@param x1 integer Start X coordinate +---@param y1 integer Start Y coordinate +---@param x2 integer End X coordinate +---@param y2 integer End Y coordinate +---@param color integer[] RGB color as {r, g, b} with 8-bit channels (0–255) +function LCDElement:draw_line(x1, y1, x2, y2, color) end + +---Draws a rectangle outline between two corner points. +---@param x1 integer Top-left X coordinate +---@param y1 integer Top-left Y coordinate +---@param x2 integer Bottom-right X coordinate +---@param y2 integer Bottom-right Y coordinate +---@param color integer[] RGB color as {r, g, b} with 8-bit channels (0–255) +function LCDElement:draw_rectangle(x1, y1, x2, y2, color) end + +---Draws a filled rectangle between two corner points. +---@param x1 integer Top-left X coordinate +---@param y1 integer Top-left Y coordinate +---@param x2 integer Bottom-right X coordinate +---@param y2 integer Bottom-right Y coordinate +---@param color integer[] RGB color as {r, g, b} with 8-bit channels (0–255) +function LCDElement:draw_rectangle_filled(x1, y1, x2, y2, color) end + +---Draws a rounded rectangle outline between two corner points. +---@param x1 integer Top-left X coordinate +---@param y1 integer Top-left Y coordinate +---@param x2 integer Bottom-right X coordinate +---@param y2 integer Bottom-right Y coordinate +---@param radius integer Corner radius in pixels +---@param color integer[] RGB color as {r, g, b} with 8-bit channels (0–255) +function LCDElement:draw_rectangle_rounded(x1, y1, x2, y2, radius, color) end + +---Draws a filled rounded rectangle between two corner points. +---@param x1 integer Top-left X coordinate +---@param y1 integer Top-left Y coordinate +---@param x2 integer Bottom-right X coordinate +---@param y2 integer Bottom-right Y coordinate +---@param radius integer Corner radius in pixels +---@param color integer[] RGB color as {r, g, b} with 8-bit channels (0–255) +function LCDElement:draw_rectangle_rounded_filled(x1, y1, x2, y2, radius, color) end + +---Draws a polygon outline using coordinate arrays. +---@param xs integer[] Array of X coordinates {x1, x2, x3, ...} +---@param ys integer[] Array of Y coordinates {y1, y2, y3, ...} +---@param color integer[] RGB color as {r, g, b} with 8-bit channels (0–255) +function LCDElement:draw_polygon(xs, ys, color) end + +---Draws a filled polygon using coordinate arrays. +---@param xs integer[] Array of X coordinates {x1, x2, x3, ...} +---@param ys integer[] Array of Y coordinates {y1, y2, y3, ...} +---@param color integer[] RGB color as {r, g, b} with 8-bit channels (0–255) +function LCDElement:draw_polygon_filled(xs, ys, color) end + +---Draws text at the specified position. +---@param text string Text to draw +---@param x integer X coordinate +---@param y integer Y coordinate +---@param size integer Font size +---@param color integer[] RGB color as {r, g, b} with 8-bit channels (0–255) +function LCDElement:draw_text(text, x, y, size, color) end + +---Draws text at the specified position using fast rendering. +---@param text string Text to draw +---@param x integer X coordinate +---@param y integer Y coordinate +---@param size integer Font size +---@param color integer[] RGB color as {r, g, b} with 8-bit channels (0–255) +function LCDElement:draw_text_fast(text, x, y, size, color) end + +---Fills an area with a solid color (no alpha blending). +---@param x1 integer Top-left X coordinate +---@param y1 integer Top-left Y coordinate +---@param x2 integer Bottom-right X coordinate +---@param y2 integer Bottom-right Y coordinate +---@param color integer[] RGB color as {r, g, b} with 8-bit channels (0–255) +function LCDElement:draw_area_filled(x1, y1, x2, y2, color) end + +---Draws the n-th iteration of a built-in demo animation. +---@param n integer Demo iteration number +function LCDElement:draw_demo(n) end + +---Returns the time spent rendering between the last two swaps, in microseconds. +---@return integer microseconds Render time +function LCDElement:get_render_time() end + +---Returns the screen index used by low-level global GUI APIs. +---@return integer screen_index Screen index for use with gui_draw_* functions +function LCDElement:screen_index() end + +---Returns the screen width in pixels. +---@return integer width Screen width +function LCDElement:screen_width() end + +---Returns the screen height in pixels. +---@return integer height Screen height +function LCDElement:screen_height() end + +-- ============================================================================= +-- SystemElement +-- The system element is the last element in the element[] array (ele[#ele]). +-- It has no hardware-specific methods beyond the base Element class. +-- ============================================================================= + +---@class SystemElement : Element +local SystemElement = {} + +-- ============================================================================= +-- Event types +-- ============================================================================= + +---@alias EventType +---| 0 # setup (init) — runs once when the page is loaded +---| 1 # potmeter — potentiometer value changed +---| 2 # encoder — encoder rotated +---| 3 # button — button pressed or released +---| 4 # utility (mapmode) — utility/map mode event +---| 5 # midirx — MIDI message received +---| 6 # timer — periodic timer fired +---| 7 # endless — endless potentiometer rotated +---| 8 # draw — LCD screen draw event (VSN1 only) + +-- ============================================================================= +-- Global functions — MIDI +-- ============================================================================= + +---Returns the short code of the currently executing event handler. +---Possible values: "ini", "ec", "bc", "pc", "tim", "map", "mrx", "epc", "ld". +---@return string event_name Event handler short code +function event_function_name() end + +---@alias MIDI_Channel +---| -1 # auto +---| 0 # Channel 1 +---| 1 # Channel 2 +---| 2 # Channel 3 +---| 3 # Channel 4 +---| 4 # Channel 5 +---| 5 # Channel 6 +---| 6 # Channel 7 +---| 7 # Channel 8 +---| 8 # Channel 9 +---| 9 # Channel 10 +---| 10 # Channel 11 +---| 11 # Channel 12 +---| 12 # Channel 13 +---| 13 # Channel 14 +---| 14 # Channel 15 +---| 15 # Channel 16 + +---@alias MIDI_Command +---| -1 # auto +---| 176 # Control change +---| 144 # Note on +---| 128 # Note off + +---@alias MIDI_Param +---| -1 # auto +---| 0 +---| 1 +---| 2 +---| 3 +---| 4 +---| 5 +---| 6 +---| 7 +---| 8 +---| 9 +---| 10 +---| 11 +---| 12 +---| 13 +---| 14 +---| 15 +---| 16 +---| 17 +---| 18 +---| 19 +---| 20 +---| 21 +---| 22 +---| 23 +---| 24 +---| 25 +---| 26 +---| 27 +---| 28 +---| 29 +---| 30 +---| 31 +---| 32 +---| 33 +---| 34 +---| 35 +---| 36 +---| 37 +---| 38 +---| 39 +---| 40 +---| 41 +---| 42 +---| 43 +---| 44 +---| 45 +---| 46 +---| 47 +---| 48 +---| 49 +---| 50 +---| 51 +---| 52 +---| 53 +---| 54 +---| 55 +---| 56 +---| 57 +---| 58 +---| 59 +---| 60 +---| 61 +---| 62 +---| 63 +---| 64 +---| 65 +---| 66 +---| 67 +---| 68 +---| 69 +---| 70 +---| 71 +---| 72 +---| 73 +---| 74 +---| 75 +---| 76 +---| 77 +---| 78 +---| 79 +---| 80 +---| 81 +---| 82 +---| 83 +---| 84 +---| 85 +---| 86 +---| 87 +---| 88 +---| 89 +---| 90 +---| 91 +---| 92 +---| 93 +---| 94 +---| 95 +---| 96 +---| 97 +---| 98 +---| 99 +---| 100 +---| 101 +---| 102 +---| 103 +---| 104 +---| 105 +---| 106 +---| 107 +---| 108 +---| 109 +---| 110 +---| 111 +---| 112 +---| 113 +---| 114 +---| 115 +---| 116 +---| 117 +---| 118 +---| 119 +---| 120 +---| 121 +---| 122 +---| 123 +---| 124 +---| 125 +---| 126 +---| 127 + +---Sends a MIDI message. +---Pass -1 for any parameter to use the auto-configured value. +---@param channel MIDI_Channel MIDI channel (0–15, or -1 for auto) +---@param command MIDI_Command MIDI command (e.g. 144=NoteOn, 176=CC, or -1 for auto) +---@param param1 MIDI_Param First parameter (0–127, or -1 for auto) +---@param param2 MIDI_Param Second parameter (0–127, or -1 for auto) +function midi_send(channel, command, param1, param2) end + +---Sends a MIDI SysEx message. +---@param ... integer SysEx data bytes +function midi_sysex_send(...) end + +-- ============================================================================= +-- Global functions — LED +-- ============================================================================= + +---Sets LED color by layer for a specific element LED. +---@param led_index integer Hardware LED index (use led_address_get to resolve) +---@param layer integer LED layer +---@param red integer Red component (0–255) +---@param green integer Green component (0–255) +---@param blue integer Blue component (0–255) +function led_color(led_index, layer, red, green, blue) end + +---Sets the LED phase/intensity value for a specific LED and layer. +---@param led_index integer Hardware LED index +---@param layer integer LED layer +---@param value integer Phase/intensity value (0–255) +function led_value(led_index, layer, value) end + +---Sets the default red LED component for the module. +---@param value? integer If provided, sets the red value (0–255) +---@return integer red Current red value +function led_default_red(value) end + +---Sets the default green LED component for the module. +---@param value? integer If provided, sets the green value (0–255) +---@return integer green Current green value +function led_default_green(value) end + +---Sets the default blue LED component for the module. +---@param value? integer If provided, sets the blue value (0–255) +---@return integer blue Current blue value +function led_default_blue(value) end + +---Sets the LED animation rate for a specific LED and layer. +---@param led_index integer Hardware LED index +---@param layer integer LED layer +---@param value integer Animation rate +function led_animation_rate(led_index, layer, value) end + +---Sets the LED animation type/shape for a specific LED and layer. +---@param led_index integer Hardware LED index +---@param layer integer LED layer +---@param value integer Animation type +function led_animation_type(led_index, layer, value) end + +-- ============================================================================= +-- Global functions — Navigation & Pages +-- ============================================================================= + +---Returns the current page number (0-based). +---@return integer page Current page index +function page_current() end + +---Loads a specific page. +---@param page integer Page index to load +function page_load(page) end + +---Loads the next page. +function page_next() end + +---Loads the previous page. +function page_prev() end + +-- ============================================================================= +-- Global functions — Module info +-- ============================================================================= + +---Returns the X position of this module in the grid. +---@return integer x Module X coordinate +function module_position_x() end + +---Returns the Y position of this module in the grid. +---@return integer y Module Y coordinate +function module_position_y() end + +---Returns the rotation of this module. +---@return integer rotation Module rotation (0, 90, 180, 270) +function module_rotation() end + +---Returns the number of elements on this module. +---@return integer count Element count +function element_count() end + +-- ============================================================================= +-- Global functions — Timers +-- ============================================================================= + +---Starts a periodic timer for an element. +---@param element_index integer Element index +---@param period integer Timer period in milliseconds +function timer_start(element_index, period) end + +---Stops the timer for an element. +---@param element_index integer Element index +function timer_stop(element_index) end + +-- ============================================================================= +-- Global functions — Events +-- ============================================================================= + +---Triggers an event on a specific element. +---@param element_index integer Element index +---@param event_type integer Event type +function event_trigger(element_index, event_type) end + +-- ============================================================================= +-- Global functions — Keyboard & Mouse +-- ============================================================================= + +---Sends a keyboard HID report. +function keyboard_send() end + +---Sends a mouse button HID report. +---@param button integer Mouse button +---@param state integer Button state +function mouse_button_send(button, state) end + +---Sends a mouse movement HID report. +---@param axis integer Movement axis +---@param position integer Movement amount +function mouse_move_send(axis, position) end + +-- ============================================================================= +-- Global functions — Utilities +-- ============================================================================= + +---Maps and saturates a value from one range to another. +---@param value number Input value +---@param from_min number Source range minimum +---@param from_max number Source range maximum +---@param to_min number Target range minimum +---@param to_max number Target range maximum +---@return number mapped Mapped and clamped value +function map_saturate(value, from_min, from_max, to_min, to_max) end + +---Clamps a value to [min, max]. +---@param value number Input value +---@param min number Minimum bound +---@param max number Maximum bound +---@return number clamped Clamped value +function limit(value, min, max) end + +---Returns the sign of a number (-1, 0, or 1). +---@param value number Input value +---@return integer sign -1, 0, or 1 +function sign(value) end + +---Returns a random integer between 0 and 255. +---@return integer value Random byte (0–255) +function random8() end + +---Calculates segment values for multi-segment LED elements. +---@param segment integer Segment index +---@param value integer Current value +---@param min integer Minimum value +---@param max integer Maximum value +---@return integer segment_value Calculated segment value (0–255) +function segment_calculate(segment, value, min, max) end + +-- ============================================================================= +-- Global functions — Communication +-- ============================================================================= + +---Sends a string message via the package protocol. +---@param message string Message to send +function package_send(message) end + +---Sends a string message via WebSocket. +---@param message string Message to send +function websocket_send(message) end + +---Sends Lua code for immediate execution on a remote module. +---@param x integer Target module X coordinate +---@param y integer Target module Y coordinate +---@param lua_code string Lua code to execute +function immediate_send(x, y, lua_code) end + +-- ============================================================================= +-- Global functions — Element naming +-- ============================================================================= + +---Sets the name of an element. +---@param element_index integer Element index +---@param name string Name to assign +function element_name_set(element_index, name) end + +---Gets the name of an element. +---@param element_index integer Element index +---@return string name Element name +function element_name_get(element_index) end + +---Sends an element name update notification. +---@param element_index integer Element index +function element_name_send(element_index) end diff --git a/build-scripts/download-luals.js b/build-scripts/download-luals.js new file mode 100644 index 000000000..829b6127d --- /dev/null +++ b/build-scripts/download-luals.js @@ -0,0 +1,129 @@ +#!/usr/bin/env node +/** + * Downloads lua-language-server binaries for the current build targets. + * + * Usage: + * node build-scripts/download-luals.js # Auto-detect targets + * node build-scripts/download-luals.js darwin-arm64 # Specific target + * node build-scripts/download-luals.js darwin-arm64 darwin-x64 # Multiple targets + * + * Targets auto-detected per platform: + * macOS → darwin-arm64, darwin-x64 (universal build) + * Windows → win32-x64 + * Linux → linux-x64 + */ + +const fs = require("node:fs"); +const path = require("node:path"); +const { execSync } = require("node:child_process"); + +const LUALS_VERSION = "3.17.1"; +const RELEASE_BASE = `https://github.com/LuaLS/lua-language-server/releases/download/${LUALS_VERSION}`; +const BUILD_ASSETS_DIR = path.resolve(__dirname, "..", "build-assets"); + +const VALID_TARGETS = [ + "darwin-arm64", + "darwin-x64", + "win32-x64", + "win32-ia32", + "linux-x64", + "linux-arm64", +]; + +function getDefaultTargets() { + switch (process.platform) { + case "darwin": + // macOS electron-builder targets both arm64 and x64 + return ["darwin-arm64", "darwin-x64"]; + case "win32": + return ["win32-x64"]; + case "linux": + return ["linux-x64"]; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } +} + +function getTargets() { + const args = process.argv.slice(2).filter((a) => !a.startsWith("-")); + const targets = args.length > 0 ? args : getDefaultTargets(); + + for (const t of targets) { + if (!VALID_TARGETS.includes(t)) { + throw new Error( + `Invalid target: ${t}\nValid targets: ${VALID_TARGETS.join(", ")}`, + ); + } + } + + return targets; +} + +function assetFilename(target) { + const ext = target.startsWith("win32") ? "zip" : "tar.gz"; + return `lua-language-server-${LUALS_VERSION}-${target}.${ext}`; +} + +async function downloadFile(url, dest) { + const res = await fetch(url, { redirect: "follow" }); + if (!res.ok) { + throw new Error(`Download failed: HTTP ${res.status} ${res.statusText}`); + } + fs.writeFileSync(dest, Buffer.from(await res.arrayBuffer())); +} + +function extractArchive(archivePath, destDir) { + fs.mkdirSync(destDir, { recursive: true }); + // bsdtar (macOS/Windows) and GNU tar (Linux) both auto-detect format + execSync(`tar xf "${archivePath}" -C "${destDir}"`, { stdio: "inherit" }); +} + +async function processTarget(target) { + const dirName = `lua-language-server-${LUALS_VERSION}-${target}`; + const destDir = path.join(BUILD_ASSETS_DIR, dirName); + + if (fs.existsSync(destDir)) { + console.log(`[${target}] Already exists, skipping.`); + return; + } + + const filename = assetFilename(target); + const archivePath = path.join(BUILD_ASSETS_DIR, filename); + const url = `${RELEASE_BASE}/${filename}`; + + console.log(`[${target}] Downloading ${url}`); + await downloadFile(url, archivePath); + + console.log(`[${target}] Extracting to ${dirName}/`); + extractArchive(archivePath, destDir); + fs.unlinkSync(archivePath); + + // Ensure binary is executable on Unix + if (!target.startsWith("win32")) { + const bin = path.join(destDir, "bin", "lua-language-server"); + if (fs.existsSync(bin)) { + fs.chmodSync(bin, 0o755); + } + } + + console.log(`[${target}] Ready.`); +} + +async function main() { + fs.mkdirSync(BUILD_ASSETS_DIR, { recursive: true }); + const targets = getTargets(); + console.log( + `lua-language-server v${LUALS_VERSION} — targets: ${targets.join(", ")}\n`, + ); + + for (const target of targets) { + await processTarget(target); + } + + console.log("\nAll targets downloaded."); +} + +main().catch((err) => { + console.error("Error:", err.message); + process.exit(1); +}); diff --git a/MIGRATION_LORE.MD b/docs/MIGRATION_LORE.MD similarity index 100% rename from MIGRATION_LORE.MD rename to docs/MIGRATION_LORE.MD diff --git a/docs/luals-integration-status.md b/docs/luals-integration-status.md new file mode 100644 index 000000000..9ce8a971c --- /dev/null +++ b/docs/luals-integration-status.md @@ -0,0 +1,215 @@ +# LuaLS + Monaco Integration + +**Branch:** `kkerti-luaLS` + +--- + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────┐ +│ Electron Main Process │ +│ │ +│ ipcmain_luals.ts │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ WebSocket server (port 8089) │ │ +│ │ └─ per connection: │ │ +│ │ spawn lua-language-server --stdio │ │ +│ │ bridge WS ↔ stdio (vscode-ws-jsonrpc) │ │ +│ │ inject config via --configpath │ │ +│ └───────────────────────────────────────────────┘ │ +│ │ +│ .luarc.json (written to /tmp at startup) │ +│ ├─ runtime: Lua 5.4 │ +│ ├─ diagnostics.globals: ["self", "element"] │ +│ ├─ workspace.library: [lua-annotations/] │ +│ └─ completion.callSnippet: "Replace" │ +│ │ +│ build-assets/lua-annotations/grid-api.lua │ +│ └─ @meta file with classes, aliases, functions │ +└──────────────────┬──────────────────────────────────┘ + │ WebSocket (ws://localhost:8089) + │ LSP JSON-RPC messages +┌──────────────────▼──────────────────────────────────┐ +│ Renderer Process │ +│ │ +│ monaco-luals-client.ts │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ JsonRpcConnection (raw WebSocket) │ │ +│ │ ├─ initialize / initialized handshake │ │ +│ │ ├─ textDocument/didOpen, didChange, didClose│ │ +│ │ ├─ textDocument/completion → Monaco │ │ +│ │ ├─ textDocument/hover → Monaco │ │ +│ │ ├─ textDocument/signatureHelp → Monaco │ │ +│ │ ├─ textDocument/semanticTokens/full → Monaco│ │ +│ │ └─ workspace/configuration handler │ │ +│ └───────────────────────────────────────────────┘ │ +│ │ +│ monaco.ts │ +│ ├─ Monarch tokenizer: syntax highlighting │ +│ ├─ Old hand-rolled completion/hover (coexists) │ +│ ├─ Theme + semantic highlighting enabled │ +│ └─ initLuaLSP() called at init (non-blocking) │ +└─────────────────────────────────────────────────────┘ +``` + +LuaLS runs as a child process on the **main thread**. The LSP client lives in the **renderer** because Monaco providers must be registered in the same JS context as the editor. The WebSocket bridge is a dumb pipe — no interception, just JSON-RPC bytes between the renderer's WS client and LuaLS's stdio. + +--- + +## What's Working + +### Annotation-driven intelligence + +The single most impactful piece: `build-assets/lua-annotations/grid-api.lua`. + +Once LuaLS reads this `---@meta` file (via `workspace.library` in `.luarc.json`), everything flows naturally: + +- **Completions** — global functions appear in the suggestion list +- **Snippets** — accepting a function completion inserts params with tab-stop placeholders +- **Hover** — full signature + `@param` / `@return` docs rendered as markdown +- **Signature help** — typing `(` or `,` shows the active parameter +- **Enum/alias values** — `@alias` with `---|` entries produces string completions when LuaLS knows the expected type +- **Class fields** — `@class` fields appear when constructing a table for that parameter + +### Completion provider + +- Trigger characters: `.` `:` `"` `'` `(` `,` +- Respects server `textEdit.range` (critical for string/enum completions where the replace span includes quotes) +- Full LSP kind mapping (1–25) including `EnumMember` +- Snippet insertion via `insertTextFormat === 2` +- `resolveCompletionItem` for lazy-loaded docs + +### Hover, Signature help, Semantic tokens + +- Hover delegates to `textDocument/hover`, converts LSP ranges to Monaco ranges +- Signature help triggers on `(` and `,`, retriggers on `)` +- Semantic tokens: protocol works, but Monaco's built-in themes have limited semantic token color rules — this is a theme/styling gap, not a protocol problem + +### WebSocket bridge + +- Clean pipe: no message interception, no rewriting +- Config injected via `--configpath` pointing to a generated `.luarc.json` +- `workspace/configuration` responses also return `callSnippet: "Replace"` for runtime consistency + +--- + +## Lessons Learned + +### Annotations: just plain `.lua` files + +The initial attempt tried to resolve annotation files from `@intechstudio/grid-protocol/annotations` — a path that didn't exist. LuaLS needs plain `.lua` files with `---@meta` headers in a folder passed via `Lua.workspace.library`. No special format, no JSON, no build step. + +The breakthrough was creating `build-assets/lua-annotations/grid-api.lua` with `---@meta` at the top and writing normal Lua function stubs with EmmyLua-style annotations. + +### LuaLS config: two paths that must agree + +1. **`.luarc.json`** via `--configpath` (read at startup) +2. **`workspace/configuration`** request (pulled at runtime) + +If these disagree, behavior is unpredictable. For example, `completion.callSnippet: "Replace"` must be in both — LuaLS may re-pull config at runtime and an empty `{}` response would reset it. + +### Snippet completions: three things must align + +1. `.luarc.json`: `completion.callSnippet: "Replace"` +2. `workspace/configuration` handler returns: `{ completion: { callSnippet: "Replace" } }` +3. Client declares `snippetSupport: true` in `initialize` capabilities + +Without all three, LuaLS sends plain text completions with no parentheses. + +### Enum suggestions need trigger characters + range handling + +- `"` and `'` as trigger characters so completions fire when the user starts typing a string literal +- Use the server's `textEdit.range` instead of `getWordUntilPosition` — string completions include the surrounding quotes in the replace range + +### Monaco standalone ≠ VS Code + +Several things that "just work" in VS Code require manual wiring in standalone Monaco: + +- No built-in LSP client — had to write `JsonRpcConnection` from scratch +- No `vscode-languageclient` equivalent that auto-registers providers +- `semanticHighlighting.enabled` must be passed as an editor construction option +- Theme semantic token colors require workarounds +- Document sync is manual (`didOpen`/`didChange`/`didClose`) + +We opted for a minimal hand-rolled approach to avoid the heavy `@codingame/monaco-vscode-api` dependency chain. + +--- + +## File Map + +| File | Role | +| ---------------------------------------------------- | --------------------------------------------------------------- | +| `src/electron/ipcmain_luals.ts` | Spawns LuaLS, bridges stdio ↔ WS, writes `.luarc.json` | +| `src/electron/main.ts` | Calls `startLuaLSServer()` / `stopLuaLSServer()` | +| `src/renderer/lib/monaco-luals-client.ts` | LSP client: JSON-RPC, document sync, all providers | +| `src/renderer/lib/monaco.ts` | Monarch tokenizer, theme, editor factory, calls `initLuaLSP()` | +| `build-assets/lua-annotations/grid-api.lua` | `---@meta` annotations for the Grid Lua API | +| `build-scripts/download-luals.js` | Downloads platform-specific LuaLS binaries from GitHub releases | +| `build-assets/lua-language-server--/` | LuaLS binary (gitignored, auto-downloaded per platform) | + +--- + +## TODO + +### Annotations + +- [ ] Add all ~72 global functions to `grid-api.lua` (currently a test subset) +- [ ] Add `Element` class with method annotations (~53 methods) +- [ ] Declare `self` and `element` as `Element` globals +- [ ] Consider auto-generating annotations from `grid.get_luadocs()` or `grid-fw` source + +### Semantic tokens / coloring + +- [ ] Investigate Monaco theme API for semantic token color overrides +- [ ] Alternatively: use a `TokensProviderFactory` to map LuaLS semantic types to Monarch-compatible scopes + +### Build / packaging + +- [x] Bundle LuaLS binary in `extraResources` via `electron-builder-config.js` +- [x] Handle packaged app paths in `resolveLualsBinary()` +- [x] Add macOS x64, Linux, Windows binaries — `download-luals.js` auto-detects platform +- [x] Download script integrated into `postinstall` hook and CI workflow +- [ ] Investigate LuaLS WASM as cross-platform alternative + +### LSP client improvements + +- [ ] Wire `textDocument/publishDiagnostics` to `monaco_editor.setModelMarkers` +- [ ] Debounce `didChange` notifications +- [ ] WebSocket reconnection on drop +- [ ] Multi-document support (multiple virtual URIs) +- [ ] Decide: keep or remove old hand-rolled completion/hover providers from `monaco.ts` + +--- + +## Progress Log + +### 2026-03-19 — Initial integration attempt + +First pass at integrating LuaLS with Monaco. Created the WebSocket bridge (`ipcmain_luals.ts`), hand-rolled JSON-RPC client in the renderer, and rewrote `monaco.ts` with LSP providers. The annotation path from `grid-protocol` didn't exist — resolved by creating `build-assets/lua-annotations/grid-api.lua` with `---@meta` stubs. Decided to revert the large diff and re-apply incrementally. Kept the Monarch tokenizer for syntax highlighting alongside LuaLS intelligence. + +Key commits: `b66e4b577` (consume luadocs in Monaco), `b4ddb8240` (allow symlinking grid-protocol). + +### 2026-03-25 — Cross-platform LuaLS binary packaging + +**PR goal:** Verify that the new LuaLS integration brings meaningful improvements to Grid Editor across all platforms. + +Changes: + +1. **`build-scripts/download-luals.js`** (new) — Downloads platform-specific LuaLS v3.17.1 binaries from GitHub releases. Auto-detects targets per OS. Skips if already exists. + +2. **`src/electron/ipcmain_luals.ts`** — Dynamic platform/arch resolution via `getAssetsBase()` + `resolveLualsBinary()`. Handles packaged vs dev paths and `.exe` on Windows. + +3. **`electron-builder-config.js`** — `extraResources` dynamically includes all `lua-language-server-*` directories and `lua-annotations/` outside ASAR. + +4. **`.github/workflows/build-matrix.yml`** — Added "Download LuaLS binaries" step after `npm ci`. + +5. **`package.json`** — Added `download:luals` script, wired into `postinstall`. + +CI matrix: + +| Runner | Downloads | Packaged into | +| ---------------- | ---------------------- | ---------------- | +| `macos-latest` | `darwin-arm64` + `x64` | arm64 + x64 DMGs | +| `windows-latest` | `win32-x64` | NSIS installer | +| `ubuntu-22.04` | `linux-x64` | AppImage | diff --git a/electron-builder-config.js b/electron-builder-config.js index 384a88b0e..1a1994fe6 100644 --- a/electron-builder-config.js +++ b/electron-builder-config.js @@ -1,4 +1,6 @@ const dotenv = require("dotenv"); +const fs = require("fs"); +const path = require("path"); dotenv.config(); function productNameByWorkflow() { @@ -32,6 +34,18 @@ const config = { from: "src/renderer/assets/**/*", to: "assets", }, + { + from: "build-assets/lua-annotations", + to: "lua-annotations", + }, + ...fs + .readdirSync(path.join(__dirname, "build-assets")) + .filter( + (d) => + d.startsWith("lua-language-server-") && + fs.statSync(path.join(__dirname, "build-assets", d)).isDirectory(), + ) + .map((dir) => ({ from: `build-assets/${dir}`, to: dir })), ], files: ["**/*"], win: { diff --git a/package-lock.json b/package-lock.json index 0efc8ef85..37573188d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "grid-editor", - "version": "1.6.5", + "version": "1.6.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "grid-editor", - "version": "1.6.5", + "version": "1.6.6", "hasInstallScript": true, "dependencies": { - "@intechstudio/grid-protocol": "1.20260302.1321", + "@intechstudio/grid-protocol": "1.20260323.1453", "@intechstudio/grid-uikit": "1.20260303.1559", "@intechstudio/profile-cloud-webcomponent": "1.20251107.1414", "adm-zip": "^0.5.10", @@ -29,6 +29,7 @@ "marked": "^15.0.12", "mixpanel-browser": "^2.47.0", "monaco-editor": "^0.55.1", + "monaco-languageclient": "^10.7.0", "node-fetch": "^2.6.7", "node-fetch-progress": "^1.0.2", "number-to-words": "^1.2.4", @@ -44,6 +45,8 @@ "typescript": "^5.5.0", "uuid": "^9.0.0", "vite": "^7.3.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-ws-jsonrpc": "^3.5.0", "ws": "^7.5.8", "yauzl": "^3.2.1" }, @@ -526,6 +529,434 @@ "license": "MIT", "peer": true }, + "node_modules/@codingame/monaco-vscode-api": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-api/-/monaco-vscode-api-25.1.2.tgz", + "integrity": "sha512-K04QcQA+Zb0KXucBAK/BGCT5dldiwIqdUbBQq7yuLvBLbof3cP1WSUuxasMHGYwM0MWyzIAsDtyAYMS7is8ZuA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-base-service-override": "25.1.2", + "@codingame/monaco-vscode-environment-service-override": "25.1.2", + "@codingame/monaco-vscode-extensions-service-override": "25.1.2", + "@codingame/monaco-vscode-files-service-override": "25.1.2", + "@codingame/monaco-vscode-host-service-override": "25.1.2", + "@codingame/monaco-vscode-layout-service-override": "25.1.2", + "@codingame/monaco-vscode-quickaccess-service-override": "25.1.2", + "@vscode/iconv-lite-umd": "0.7.1", + "dompurify": "3.3.1", + "jschardet": "3.1.4", + "marked": "14.0.0" + } + }, + "node_modules/@codingame/monaco-vscode-api/node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/@codingame/monaco-vscode-api/node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@codingame/monaco-vscode-base-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-base-service-override/-/monaco-vscode-base-service-override-25.1.2.tgz", + "integrity": "sha512-OwYs6h1ATUAeMmX+Q1c8esTG7GLMqniBs+fLEr1/9b/ciY485ArKo5UvrUxVPDtRNy/7F06vRW9IUCq9iKP14w==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-bulk-edit-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-bulk-edit-service-override/-/monaco-vscode-bulk-edit-service-override-25.1.2.tgz", + "integrity": "sha512-+EfSzjiFakCf0IIJKPZrHVGioq5N8GBsp51bXuKBR5J/B58cUaJY0Dc12PNTSpgAusAGOppUIOSBqUk4F/7IaQ==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-configuration-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-configuration-service-override/-/monaco-vscode-configuration-service-override-25.1.2.tgz", + "integrity": "sha512-oeoZ3WtM42zHA1IWHrx9UGEfE+TixE+G8Bl9M9bjgFj1EROnkB5yOfELwRYPo4WOEtcK1C5nvIvWIj/hL9MaLg==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-files-service-override": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-editor-api": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-editor-api/-/monaco-vscode-editor-api-25.1.2.tgz", + "integrity": "sha512-dVXoBLRN8vyFHsLY6iYISaNetZ3ispXLut0qL+jvN0e0CEFkUv1F/3EAE7myptrJSS/N1AptrRIxATT3lwFP+Q==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-editor-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-editor-service-override/-/monaco-vscode-editor-service-override-25.1.2.tgz", + "integrity": "sha512-EadvDCyWdgxOPmaIvbcVVDNjTUYuKdjYWwKbPbbcTs9t4z1/DjdE7mV3ZdT6aGh5m6zkEEUOi143l27Y5eRt+Q==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-environment-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-environment-service-override/-/monaco-vscode-environment-service-override-25.1.2.tgz", + "integrity": "sha512-8GoD3lk0CN0dIMZOrZNS/i8RCaF1YSQ6nmrf+rqneOSHG9S382EnsZZD69d4+i7JnoeyttO7Kr9KH8WOhRV6OA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-extension-api": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-extension-api/-/monaco-vscode-extension-api-25.1.2.tgz", + "integrity": "sha512-SJW/YOhjo+9MXEyzMwQMUWdJVR3Llc6pTq5JQqs6Y30v73gTrpLqtzbd9FNdCuQR8S6bUk5ScH8GL4QrVuL5FA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-extensions-service-override": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-extensions-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-extensions-service-override/-/monaco-vscode-extensions-service-override-25.1.2.tgz", + "integrity": "sha512-rTTZW2biPxcg+JumhVf2L+38C5ptvNNxiJlwz39VfXFEh6qOHtAsIMy7vIXa0uGg5/y8DNp0SnOQJP/RKhLYZA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-files-service-override": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-files-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-files-service-override/-/monaco-vscode-files-service-override-25.1.2.tgz", + "integrity": "sha512-TenLLAFIwY7keZFF8e3beUn7OVfnNINR5Noi4PVrjeeTcy6FuNH6Jghdul2JwpRAkvyJLdFMvomE2jlT6F03jQ==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-host-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-host-service-override/-/monaco-vscode-host-service-override-25.1.2.tgz", + "integrity": "sha512-lgaalpA9CUQW7i0bBwgBOK0DQNDvOo3QO3p6Rz6yVsHpgA4iMqq2d11dBDUKvuQSwIHPRu8CMHCqhQk/BQN/YA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-keybindings-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-keybindings-service-override/-/monaco-vscode-keybindings-service-override-25.1.2.tgz", + "integrity": "sha512-cp/gGyTvCTAzCYnQm0HJykXJRB0Huz8Lvq60lj5LutgWcb8S3w6dOB2Houm8dHoeUm/jOko8SQNIP8hzWN92Zw==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-files-service-override": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-cs": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-cs/-/monaco-vscode-language-pack-cs-25.1.2.tgz", + "integrity": "sha512-v0cB2uAOCwj135aGIf0arGV+DNW32lbWh04bv8ctTxcWRt1Pr2kTQ1pjfE8ynKgxabPfAk8E25/CerKSYOmZ+A==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-de": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-de/-/monaco-vscode-language-pack-de-25.1.2.tgz", + "integrity": "sha512-xA3WOt1w5jlAOnyx4PBwx+qV3vx8C8/zie29qjYbgJMxGKDkb0HfpuKUwywDA2uUMI2wJZS+PnNG00zPDoLIrw==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-es": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-es/-/monaco-vscode-language-pack-es-25.1.2.tgz", + "integrity": "sha512-1/upuO9lRJilZ3sRr0QLTpz55KYRaBWDe8wtPvghOFYOHyWgW8A4VhUQxa6L9SJgY1JkypUAm0U8WcMX2G4LnQ==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-fr": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-fr/-/monaco-vscode-language-pack-fr-25.1.2.tgz", + "integrity": "sha512-iq+xx+tv1QIMmFD0eBhFRMF4xMAsVf/HyA1WogqBofteCWeAvRE9HUjZ5JzHz7jXBPe3dLP1LOM0r0GrJZs4fQ==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-it": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-it/-/monaco-vscode-language-pack-it-25.1.2.tgz", + "integrity": "sha512-FajWCML9OR8ppLnJ0mcg+sFHEhYJl8zhb3/DHnd+pNysw8dLfetXoSWjaPnwPPpwiQgkNN1UsToZHOU9czVifQ==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-ja": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-ja/-/monaco-vscode-language-pack-ja-25.1.2.tgz", + "integrity": "sha512-NwKh0BnPgUrJkxsm0X6vY4ftnd9DjxkcnQqK+bohta6UOzm09J1EjZ6QD42fjWngxrp/xiegtrYQ9NA2q6VpoA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-ko": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-ko/-/monaco-vscode-language-pack-ko-25.1.2.tgz", + "integrity": "sha512-fvaisgfcg8YaAwnyPcGmQDLwkwqzamLQUyx9HmnwDpXw0YANzd058Kwn6bz+Vfn9MjwuMNT0nllD0qQMnpdyew==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-pl": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-pl/-/monaco-vscode-language-pack-pl-25.1.2.tgz", + "integrity": "sha512-9hDRyzFJkDia5rO9QE262JgxwP/cnalFisLFo7FQcw57ZhqzqXIdQIuwcKaHuAgzeQ6W2+A3KOLfTr3m7VZrXw==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-pt-br": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-pt-br/-/monaco-vscode-language-pack-pt-br-25.1.2.tgz", + "integrity": "sha512-7fFnqOTAJGb5RuJ4uwh9sh0JmXALuHPGOl7iL9rZkcgIuVP5y6wVDUDXq5qjiRTNSFDs7Bzh463Ir5m5D6mJbA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-qps-ploc": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-qps-ploc/-/monaco-vscode-language-pack-qps-ploc-25.1.2.tgz", + "integrity": "sha512-IFjoqrSuPtIFWb+KlPT6PFWKszzNX+TCD9drgCV6AigvBO/xfGL3QwHB68l/DLbmDbohOz4Xdkutv20wuENAeA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-ru": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-ru/-/monaco-vscode-language-pack-ru-25.1.2.tgz", + "integrity": "sha512-0uDAeXO+GllKUPhJzP893rlDhlFV1IwCu/515rBdcyegt48iGm/xAgj26V90hNz8hmB6EuM/7d8MFeklbiIpYA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-tr": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-tr/-/monaco-vscode-language-pack-tr-25.1.2.tgz", + "integrity": "sha512-MJhHxDyJEiuVLQ9+jb8MnnN9lsbJOjJjMswVCeJ7v/Q/msAhq25QYUfn0DbOIzESJE1f7crffRb5e38XP8sYWA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-zh-hans": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-zh-hans/-/monaco-vscode-language-pack-zh-hans-25.1.2.tgz", + "integrity": "sha512-c7MMrhnSLb59NxpAa8nVy9aIbxy4gVYrCpDMq8W380LOaXTYb7nueTrw8QJ5QbJBNi2P2KZoGkn2BlONuBtJJg==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-language-pack-zh-hant": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-language-pack-zh-hant/-/monaco-vscode-language-pack-zh-hant-25.1.2.tgz", + "integrity": "sha512-ARedFTM6JCluoPLJqkBcTJaQFdJNcN86OX6B8/NMApIPrnSIAfanMndpyilt8XjzUG6IH22cypR+DAlEjf48cA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-languages-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-languages-service-override/-/monaco-vscode-languages-service-override-25.1.2.tgz", + "integrity": "sha512-ipuS1V3NgXDkNrj0vBcgMBFnqo+19HVsZjjFGfPFH3x0uptP9aiWWK42wtDK3Qbu4teSjHL7WnSLrmw94rplWw==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-files-service-override": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-layout-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-layout-service-override/-/monaco-vscode-layout-service-override-25.1.2.tgz", + "integrity": "sha512-SxBGcMK3RgkGtUn7ZDl7dCoyNW0CWFQ/bfSRYUY06A0IA4JNS5jq1lhof57d0WXewm+5l8w1Spr/vMsfx1c9ig==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-localization-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-localization-service-override/-/monaco-vscode-localization-service-override-25.1.2.tgz", + "integrity": "sha512-QLj62A8XDOIQW3KjsZlNxs+sfsNNHYxWMjQMwZu/y2Vw3IIHGly2Lpn4t4SFbeaBHJQJy4i5s7NpzlbF9MbEzQ==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-log-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-log-service-override/-/monaco-vscode-log-service-override-25.1.2.tgz", + "integrity": "sha512-OoileAUtPAJ0j3RW31DFSxtOipy0EcFq+iIXEdGvoRlsQPZJ3o9ayjf1JvCXpxUjJ3QkmvQVhXsWNUFREjEFLg==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-environment-service-override": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-model-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-model-service-override/-/monaco-vscode-model-service-override-25.1.2.tgz", + "integrity": "sha512-MGz/eV1CxibLvnl6WzK6idUHJCXJOVepJvKM6Trkv5050vRe+f/o1TjCiG8PaznAypYqZvnwkTG0B7/OTizCpQ==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-monarch-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-monarch-service-override/-/monaco-vscode-monarch-service-override-25.1.2.tgz", + "integrity": "sha512-akyNHOJQRS7YHyk6kf0Encnkt+shlR+bIB84UJRUHFgSeF8s5gkDkQuFJph0YeUDWJWat+yBLUSZx2nHomdbHQ==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-quickaccess-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-quickaccess-service-override/-/monaco-vscode-quickaccess-service-override-25.1.2.tgz", + "integrity": "sha512-7IIrXnwHiF3w9d9p9kspEUz/LCibMLUztmRpGdZQfFtWBJw043q7rk8V1O42KdXr1hVg9IR5vfffwjy9nbiiUg==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-textmate-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-textmate-service-override/-/monaco-vscode-textmate-service-override-25.1.2.tgz", + "integrity": "sha512-AL0FtSQBW+1vtoXYQvUqB2hfWojpK73Kq/n6KuNXxjLF/XBJ5FpeeZDfrBfwhWPPoHuBTsaFUCQy4L8xQgbVlA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-files-service-override": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-theme-defaults-default-extension": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-theme-defaults-default-extension/-/monaco-vscode-theme-defaults-default-extension-25.1.2.tgz", + "integrity": "sha512-0vTMFiC89YSDSmjFckuQBUKwRuFNtsILNO3k0PBiSLN/MW+VDItjJpiVLXC42+rUWlGgY2lYxOneGVa5slCV1w==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-theme-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-theme-service-override/-/monaco-vscode-theme-service-override-25.1.2.tgz", + "integrity": "sha512-hsTwl6YYTiheFuQMmCmiEGLIdIdgYaf8Z85XWyxe6YgPtDaYGnp0fGSOXKA9/bf0JtuynzoLKtUUfDupK/A7Tw==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-files-service-override": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-view-banner-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-view-banner-service-override/-/monaco-vscode-view-banner-service-override-25.1.2.tgz", + "integrity": "sha512-zhujHd1PQ6rRXsC2OQGrx/282G2v3lpPFl9heDFGKzpdj5119SgcW+B9p/MwJ1qF3LJpuRRgefNiQtqC/KT1eA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-view-common-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-view-common-service-override/-/monaco-vscode-view-common-service-override-25.1.2.tgz", + "integrity": "sha512-4Po/YaHUvVf4VmhVCZmM2lc/flOptiWSM140bIRNpMcfH0VwihYg15CcDeu1Oc+6DaauzsG3u59GtEvlMmJ9Zw==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-bulk-edit-service-override": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-view-status-bar-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-view-status-bar-service-override/-/monaco-vscode-view-status-bar-service-override-25.1.2.tgz", + "integrity": "sha512-Jp9ytLaWZ6evabTPtG3Mu3dFx+7WTIPz69BsGpl9PnU0kiSWUqQhPSob0Jz7E2qmMj0ZcNv2Wqvm6bMBu5OyrA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-view-title-bar-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-view-title-bar-service-override/-/monaco-vscode-view-title-bar-service-override-25.1.2.tgz", + "integrity": "sha512-NVYtTAFR35NV/Fx7tSlbASicvpAjK5A14fmxF7/LJJN8ZmzhA/P3Y+UzhqOQl6/VcPV4pAMU0Z7Sicgwbn37dw==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-views-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-views-service-override/-/monaco-vscode-views-service-override-25.1.2.tgz", + "integrity": "sha512-LfzlztsvobdP5L5EvJ/rqSEgy5fEVmrkMqRteuhEtNGd4hnmdBoX8W7BNMBPff6d4NfCK74pGHJF57RyT4Iixg==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-keybindings-service-override": "25.1.2", + "@codingame/monaco-vscode-layout-service-override": "25.1.2", + "@codingame/monaco-vscode-quickaccess-service-override": "25.1.2", + "@codingame/monaco-vscode-view-common-service-override": "25.1.2" + } + }, + "node_modules/@codingame/monaco-vscode-workbench-service-override": { + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-workbench-service-override/-/monaco-vscode-workbench-service-override-25.1.2.tgz", + "integrity": "sha512-2LMHr+na03FhOAaXpIGmamq9hf7e4wt2kULn8NqNZRd3i+0v1tx/TSSjGhsA5EkrNrFD7CMSoXayBq8tgpCq/A==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-keybindings-service-override": "25.1.2", + "@codingame/monaco-vscode-quickaccess-service-override": "25.1.2", + "@codingame/monaco-vscode-view-banner-service-override": "25.1.2", + "@codingame/monaco-vscode-view-common-service-override": "25.1.2", + "@codingame/monaco-vscode-view-status-bar-service-override": "25.1.2", + "@codingame/monaco-vscode-view-title-bar-service-override": "25.1.2" + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -2102,9 +2533,9 @@ } }, "node_modules/@intechstudio/grid-protocol": { - "version": "1.20260302.1321", - "resolved": "https://registry.npmjs.org/@intechstudio/grid-protocol/-/grid-protocol-1.20260302.1321.tgz", - "integrity": "sha512-NOsq4uO7W9BnmF77GTOfv8CWClKCSU+cl6M/OZkdOGQEnLZ+gQrRSOWKQ4wBdGtMY+U9PfZjpCU8LHU2MwM9ng==", + "version": "1.20260323.1453", + "resolved": "https://registry.npmjs.org/@intechstudio/grid-protocol/-/grid-protocol-1.20260323.1453.tgz", + "integrity": "sha512-C4/DFt8aH6d+G4SB+ea+5ZwxUo0T2ZPrZDSDAXk+ehKtzdxVGcjN73Ps7u2HfqN3UC5k+x4BSNJLJYk8ktbVrw==", "dependencies": { "@wasm-fmt/lua_fmt": "^0.2.0" } @@ -4026,6 +4457,12 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vscode/iconv-lite-umd": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.1.tgz", + "integrity": "sha512-tK6k0DXFHW7q5+GGuGZO+phpAqpxO4WXl+BLc/8/uOk3RsM2ssAL3CQUQDb1TGfwltjsauhN6S4ghYZzs4sPFw==", + "license": "MIT" + }, "node_modules/@wasm-fmt/lua_fmt": { "version": "0.2.0", "license": "MIT" @@ -9212,6 +9649,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jschardet": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz", + "integrity": "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==", + "license": "LGPL-2.1+", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/jsesc": { "version": "3.1.0", "license": "MIT", @@ -9888,6 +10334,52 @@ "node": ">= 18" } }, + "node_modules/monaco-languageclient": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/monaco-languageclient/-/monaco-languageclient-10.7.0.tgz", + "integrity": "sha512-oA5cOFixkF4bspVL2zMSn48LvlNR/Cu3vJ8MCVam3PdjobSULGgHtOASuZIi3FgWK42X1z8/6hrG0LCjvNu1Hw==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "^25.1.2", + "@codingame/monaco-vscode-configuration-service-override": "^25.1.2", + "@codingame/monaco-vscode-editor-api": "^25.1.2", + "@codingame/monaco-vscode-editor-service-override": "^25.1.2", + "@codingame/monaco-vscode-extension-api": "^25.1.2", + "@codingame/monaco-vscode-extensions-service-override": "^25.1.2", + "@codingame/monaco-vscode-language-pack-cs": "^25.1.2", + "@codingame/monaco-vscode-language-pack-de": "^25.1.2", + "@codingame/monaco-vscode-language-pack-es": "^25.1.2", + "@codingame/monaco-vscode-language-pack-fr": "^25.1.2", + "@codingame/monaco-vscode-language-pack-it": "^25.1.2", + "@codingame/monaco-vscode-language-pack-ja": "^25.1.2", + "@codingame/monaco-vscode-language-pack-ko": "^25.1.2", + "@codingame/monaco-vscode-language-pack-pl": "^25.1.2", + "@codingame/monaco-vscode-language-pack-pt-br": "^25.1.2", + "@codingame/monaco-vscode-language-pack-qps-ploc": "^25.1.2", + "@codingame/monaco-vscode-language-pack-ru": "^25.1.2", + "@codingame/monaco-vscode-language-pack-tr": "^25.1.2", + "@codingame/monaco-vscode-language-pack-zh-hans": "^25.1.2", + "@codingame/monaco-vscode-language-pack-zh-hant": "^25.1.2", + "@codingame/monaco-vscode-languages-service-override": "^25.1.2", + "@codingame/monaco-vscode-localization-service-override": "^25.1.2", + "@codingame/monaco-vscode-log-service-override": "^25.1.2", + "@codingame/monaco-vscode-model-service-override": "^25.1.2", + "@codingame/monaco-vscode-monarch-service-override": "^25.1.2", + "@codingame/monaco-vscode-textmate-service-override": "^25.1.2", + "@codingame/monaco-vscode-theme-defaults-default-extension": "^25.1.2", + "@codingame/monaco-vscode-theme-service-override": "^25.1.2", + "@codingame/monaco-vscode-views-service-override": "^25.1.2", + "@codingame/monaco-vscode-workbench-service-override": "^25.1.2", + "vscode": "npm:@codingame/monaco-vscode-extension-api@^25.1.2", + "vscode-languageclient": "~9.0.1", + "vscode-languageserver-protocol": "~3.17.5", + "vscode-ws-jsonrpc": "~3.5.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, "node_modules/ms": { "version": "2.1.3", "license": "MIT" @@ -13943,6 +14435,99 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vscode": { + "name": "@codingame/monaco-vscode-extension-api", + "version": "25.1.2", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-extension-api/-/monaco-vscode-extension-api-25.1.2.tgz", + "integrity": "sha512-SJW/YOhjo+9MXEyzMwQMUWdJVR3Llc6pTq5JQqs6Y30v73gTrpLqtzbd9FNdCuQR8S6bUk5ScH8GL4QrVuL5FA==", + "license": "MIT", + "dependencies": { + "@codingame/monaco-vscode-api": "25.1.2", + "@codingame/monaco-vscode-extensions-service-override": "25.1.2" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "license": "MIT", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageclient/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-ws-jsonrpc": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/vscode-ws-jsonrpc/-/vscode-ws-jsonrpc-3.5.0.tgz", + "integrity": "sha512-13ZDy7Od4AfEPK2HIfY3DtyRi4FVsvFql1yobVJrpIoHOKGGJpIjVvIJpMxkrHzCZzWlYlg+WEu2hrYkCTvM0Q==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "~8.2.1" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/vscode-ws-jsonrpc/node_modules/vscode-jsonrpc": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", + "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/walker": { "version": "1.0.8", "license": "Apache-2.0", diff --git a/package.json b/package.json index 5bb4ba942..58dec34d9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "grid-editor", "productName": "Grid Editor", "description": "Powerful Editor software for Intech Studio products.", - "version": "1.6.5", + "version": "1.6.6", "engines": { "node": ">=24.0.0", "npm": ">=11.0.0" @@ -39,7 +39,7 @@ "vitest": "^4.0.18" }, "dependencies": { - "@intechstudio/grid-protocol": "1.20260302.1321", + "@intechstudio/grid-protocol": "1.20260323.1453", "@intechstudio/grid-uikit": "1.20260303.1559", "@intechstudio/profile-cloud-webcomponent": "1.20251107.1414", "adm-zip": "^0.5.10", @@ -59,6 +59,7 @@ "marked": "^15.0.12", "mixpanel-browser": "^2.47.0", "monaco-editor": "^0.55.1", + "monaco-languageclient": "^10.7.0", "node-fetch": "^2.6.7", "node-fetch-progress": "^1.0.2", "number-to-words": "^1.2.4", @@ -74,6 +75,8 @@ "typescript": "^5.5.0", "uuid": "^9.0.0", "vite": "^7.3.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-ws-jsonrpc": "^3.5.0", "ws": "^7.5.8", "yauzl": "^3.2.1" }, @@ -86,10 +89,11 @@ "export:alpha": "cross-env VITE_BUILD_ENV=alpha run-s s:build e:builder", "export:local": "run-s clean-up s:build e:builder:local", "clean-up": "rm -rf dist && rm -rf dist-web && rm -rf build", + "download:luals": "node build-scripts/download-luals.js", "rebuild": "./node_modules/.bin/electron-rebuild.cmd", "s:build": "electron-vite build", "preinstall": "node ./build-scripts/check-node-version.js", - "postinstall": "electron-builder install-app-deps", + "postinstall": "electron-builder install-app-deps && node build-scripts/download-luals.js", "test": "vitest run", "electron:dev": "cross-env NODE_OPTIONS=--max_old_space_size=8198 VITE_BUILD_TARGET=electron VITE_BUILD_ENV=development electron-vite dev --watch", "electron-dev": "npm run electron:dev", diff --git a/playwright-tests/tests/blocksTests.spec.js b/playwright-tests/tests/blocksTests.spec.js index 698776ade..4249f7472 100644 --- a/playwright-tests/tests/blocksTests.spec.js +++ b/playwright-tests/tests/blocksTests.spec.js @@ -307,34 +307,6 @@ test.describe("Input field keyboard shortcuts", () => { }); }); -test.describe("Monaco Sugestion", () => { - test.beforeEach(async ({ page }) => { - connectModulePage = new ConnectModulePage(page); - modulePage = new ModulePage(page); - configPage = new ConfigPage(page); - keyboardActions = new KeyboardActions(page); - await page.goto(PAGE_PATH); - await connectModulePage.openVirtualModules(); - await connectModulePage.addModule("BU16"); - await configPage.turnOffMinimalistMode(); - await configPage.removeAllActions(); - }); - test("correct suggestion is visible once", async ({ page }) => { - const code = "button_"; - await configPage.addAndEditCodeBlock(code); - const buttonMax = page.getByLabel("self:button_max"); - await expect(buttonMax).toHaveCount(1); - await expect(buttonMax.first()).toBeVisible(); - }); - test("correct suggestion is visible after element[x]", async ({ page }) => { - const code = "element[2]:button_"; - await configPage.addAndEditCodeBlock(code); - const buttonMax = page.getByLabel("button_max"); - await expect(buttonMax).toHaveCount(1); - await expect(buttonMax.first()).toBeVisible(); - }); -}); - test.describe("Code block closes Modal", () => { test.beforeEach(async ({ page }) => { connectModulePage = new ConnectModulePage(page); @@ -374,19 +346,7 @@ test.describe("Code block closes Modal", () => { await configPage.closeCode(); await expect(await configPage.codeBlockModalDiscardButton).toBeVisible(); }); - test("Modal not appear and not closes code editor if monaco suggestion closed by 'esc' key ", async ({ - page, - }) => { - const code = "button_"; - await configPage.addAndEditCodeBlock(code); - const buttonMax = page.getByLabel("self:button_max"); - await buttonMax.waitFor({ state: "visible" }); - await keyboardActions.esc(); - await expect(await configPage.commitCodeButton).toBeVisible(); - await expect(await configPage.codeBlockModalDiscardButton).toBeHidden(); - }); - - test("Modal not appear if no cahnges", async () => { + test("Modal not appear if no changes", async () => { await configPage.addAndEditCodeBlock(`print("hello")`); await configPage.closeCode(); await expect(await configPage.codeBlockModalDiscardButton).toBeHidden(); diff --git a/playwright-tests/utility.js b/playwright-tests/utility.js index 645721846..cd73ff1a8 100644 --- a/playwright-tests/utility.js +++ b/playwright-tests/utility.js @@ -1,6 +1,6 @@ import { chromium } from "@playwright/test"; -export const PAGE_PATH = "http://localhost:5173"; +export const PAGE_PATH = "http://localhost:5273"; export async function initializeBrowserContext() { const browser = await chromium.launch(); diff --git a/playwright.config.js b/playwright.config.js index 4d69e7289..05ddebe31 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -50,7 +50,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { command: "npm run web-dev", - url: "http://localhost:5173", + url: "http://localhost:5273", reuseExistingServer: !process.env.CI, stdout: "pipe", }, diff --git a/renderer.vite.config.mjs b/renderer.vite.config.mjs index 889b02673..fdd4ea8df 100644 --- a/renderer.vite.config.mjs +++ b/renderer.vite.config.mjs @@ -1,8 +1,19 @@ import { svelte } from "@sveltejs/vite-plugin-svelte"; import { sveltePreprocess } from "svelte-preprocess"; import path, { resolve } from "path"; +import { realpathSync } from "fs"; export const rendererConfig = ({ outDir = "", additionalPlugins = [] }) => { + // Resolve symlinked packages (e.g. npm link) so Vite can serve their files + let gridProtocolRealPath; + try { + gridProtocolRealPath = realpathSync( + resolve(__dirname, "node_modules/@intechstudio/grid-protocol"), + ); + } catch { + // Package not linked, no extra fs.allow needed + } + return { plugins: [ svelte({ @@ -26,17 +37,37 @@ export const rendererConfig = ({ outDir = "", additionalPlugins = [] }) => { }, root: resolve(__dirname, "src/renderer"), resolve: { + preserveSymlinks: false, + dedupe: ["@intechstudio/grid-protocol"], alias: { + // Redirect @wasm-fmt/lua_fmt to the Vite-compatible entry that uses ?url for WASM + "@wasm-fmt/lua_fmt": "@wasm-fmt/lua_fmt/vite", + // Route all monaco-editor imports through @codingame/monaco-vscode-editor-api + // so the VSCode service-backed API is used (required by monaco-languageclient). + "monaco-editor": "@codingame/monaco-vscode-editor-api", $lib: path.resolve("src/renderer/lib"), "$app/environment": path.resolve( "src/renderer/lib/app-environment-shim.ts", ), }, }, + server: { + port: 5273, + fs: { + allow: [ + // Allow serving files from the project root + resolve(__dirname), + // Allow serving files from symlinked packages (npm link) + ...(gridProtocolRealPath ? [gridProtocolRealPath] : []), + ], + }, + }, target: "chrome104", envPrefix: "VITE_", + worker: { + format: "es", + }, optimizeDeps: { - exclude: ["@intechstudio/grid-protocol"], esbuildOptions: { plugins: [ { diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 39535d4c9..000000000 Binary files a/screenshot.png and /dev/null differ diff --git a/screenshotDIRECT.png b/screenshotDIRECT.png deleted file mode 100644 index 05e4af344..000000000 Binary files a/screenshotDIRECT.png and /dev/null differ diff --git a/screenshotPAGE_PATH.png b/screenshotPAGE_PATH.png deleted file mode 100644 index 82c78d0c7..000000000 Binary files a/screenshotPAGE_PATH.png and /dev/null differ diff --git a/src/electron/ipcmain_luals.ts b/src/electron/ipcmain_luals.ts new file mode 100644 index 000000000..fffc8486b --- /dev/null +++ b/src/electron/ipcmain_luals.ts @@ -0,0 +1,91 @@ +/** + * LuaLS WebSocket Bridge + * + * Spawns lua-language-server (--stdio) and bridges LSP JSON-RPC over a + * local WebSocket server. Config and annotations are pushed by the renderer + * via LSP notifications after connect. The bridge is a dumb pipe. + */ +import path from "path"; +import fs from "fs"; +import { app } from "electron"; +import log from "electron-log"; +import WebSocket from "ws"; +import { + createServerProcess, + createWebSocketConnection, + forward, +} from "vscode-ws-jsonrpc/server"; +import type { IWebSocket } from "vscode-ws-jsonrpc/socket"; + +const LUALS_PORT = 8089; + +let wss: any; + +function getAssetsBase(): string { + return app.isPackaged + ? process.resourcesPath + : path.resolve(__dirname, "../../build-assets"); +} + +function resolveLualsBinary(): string { + const base = getAssetsBase(); + const target = `${process.platform}-${process.arch}`; + const entries = fs.readdirSync(base); + const dir = entries.find( + (e) => e.startsWith("lua-language-server-") && e.endsWith(`-${target}`), + ); + if (!dir) { + throw new Error(`LuaLS binary not found for ${target} in ${base}`); + } + const bin = + process.platform === "win32" + ? "lua-language-server.exe" + : "lua-language-server"; + return path.join(base, dir, "bin", bin); +} + +export function startLuaLSServer() { + const binary = resolveLualsBinary(); + log.info("[LuaLS] Starting bridge on port", LUALS_PORT); + + wss = new (WebSocket as any).Server({ port: LUALS_PORT }); + + wss!.on("connection", (ws: any) => { + log.info("[LuaLS] Client connected"); + + const socket: IWebSocket = { + send: (content) => + ws.send(content, (err) => { + if (err) log.error("[LuaLS] Send error:", err); + }), + onMessage: (cb) => ws.on("message", (data) => cb(data)), + onError: (cb) => ws.on("error", cb), + onClose: (cb) => + ws.on("close", (code, reason) => cb(code, reason.toString())), + dispose: () => ws.close(), + }; + + const wsConnection = createWebSocketConnection(socket); + const serverConnection = createServerProcess("LuaLS", binary, ["--stdio"]); + + if (!serverConnection) { + log.error("[LuaLS] Failed to spawn lua-language-server"); + ws.close(); + return; + } + + forward(wsConnection, serverConnection); + log.info("[LuaLS] Bridge established"); + }); + + wss.on("error", (err) => { + log.error("[LuaLS] WebSocket server error:", err); + }); +} + +export function stopLuaLSServer() { + if (wss) { + wss.close(); + wss = undefined; + } +} diff --git a/src/electron/main.ts b/src/electron/main.ts index c4891dde5..ffeb095b5 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -69,6 +69,7 @@ log.info( import { serial, restartSerialCheckInterval } from "./ipcmain_serialport"; import { websocket } from "./ipcmain_websocket"; import { developerWebsocket } from "./developer_websocket"; +import { startLuaLSServer, stopLuaLSServer } from "./ipcmain_luals"; import { store } from "./main-store"; import { iconBuffer, iconSize } from "./icon"; import { firmware, findBootloaderPathNative, writeFirmwareToBootloader } from "./src/firmware"; @@ -454,6 +455,9 @@ function createWindow() { updater.mainWindow = mainWindow; updater.init(store.get("nightlyEditor"), store.get("disableAutoUpdate")); + // Start LuaLS WebSocket bridge for Monaco LSP + startLuaLSServer(); + // Setup custom log transport to forward logs to renderer setupRendererLogTransport(); @@ -492,7 +496,7 @@ function createWindow() { console.log(`here what is VITE_BUILD_ENV: ${import.meta.env.VITE_BUILD_ENV}`); if (import.meta.env.VITE_BUILD_ENV === "development") { log.info("Development Mode!"); - mainWindow.loadURL("http://localhost:5173/"); + mainWindow.loadURL("http://localhost:5273/"); mainWindow.webContents.openDevTools(); } else { // this is applicable for any non development environment, like production or test @@ -556,7 +560,7 @@ function createWindow() { if ( permission === "serial" && (details.securityOrigin == "file:///" || - details.securityOrigin == "http://localhost:5173/") + details.securityOrigin == "http://localhost:5273/") ) { return true; } @@ -568,7 +572,7 @@ function createWindow() { if ( details.deviceType === "serial" && (details.origin === "file://" || - details.origin === "http://localhost:5173") + details.origin === "http://localhost:5273") ) { return true; } @@ -1092,5 +1096,6 @@ app.on("activate", () => { // termination of application, closing the windows, used for macOS hide flag app.on("before-quit", (evt) => { log.info("before-quit evt", evt); + stopLuaLSServer(); app.quitting = true; }); diff --git a/src/electron/package/packageManager.ts b/src/electron/package/packageManager.ts index a03715e7f..b19c0de8e 100644 --- a/src/electron/package/packageManager.ts +++ b/src/electron/package/packageManager.ts @@ -895,7 +895,6 @@ async function getCompatibleGithubRelease(githubPackageName: string) { }, }, ); - console.log({ packageReleasesResponse }); const packageReleases = await packageReleasesResponse.json(); return ( packageReleases?.find((e) => { diff --git a/src/renderer/env.d.ts b/src/renderer/env.d.ts index 529115eb4..c4b380e3f 100644 --- a/src/renderer/env.d.ts +++ b/src/renderer/env.d.ts @@ -15,3 +15,10 @@ declare module "monaco-editor/esm/vs/editor/editor.worker?worker" { }; export default workerConstructor; } + +declare module "@codingame/monaco-vscode-editor-api/esm/vs/editor/editor.worker.js?worker" { + const workerConstructor: { + new (): Worker; + }; + export default workerConstructor; +} diff --git a/src/renderer/lib/monaco-init.ts b/src/renderer/lib/monaco-init.ts new file mode 100644 index 000000000..cee092410 --- /dev/null +++ b/src/renderer/lib/monaco-init.ts @@ -0,0 +1,43 @@ +/** + * Monaco + @codingame/monaco-vscode-api initialization. + * + * This module MUST be imported (and awaited) before any monaco-editor usage: + * - editor.create() + * - editor.defineTheme() + * - languages.register() + * - languages.setMonarchTokensProvider() + * + * It sets up the VSCode service layer that monaco-languageclient requires, + * using "classic" mode using Monarch tokenizer. + */ + +// Worker factory must be configured before initialize() +import "./monaco-workers"; + +import { initialize } from "@codingame/monaco-vscode-api"; +import getConfigurationServiceOverride from "@codingame/monaco-vscode-configuration-service-override"; +import getEditorServiceOverride from "@codingame/monaco-vscode-editor-service-override"; +import getFilesServiceOverride from "@codingame/monaco-vscode-files-service-override"; +import getLanguagesServiceOverride from "@codingame/monaco-vscode-languages-service-override"; +import getLogServiceOverride from "@codingame/monaco-vscode-log-service-override"; +import getModelServiceOverride from "@codingame/monaco-vscode-model-service-override"; +import getMonarchServiceOverride from "@codingame/monaco-vscode-monarch-service-override"; + +/** + * Initialize all VSCode services required for monaco-languageclient. + * Returns a promise that resolves when services are ready. + */ +export const monacoReady: Promise = initialize({ + ...getLogServiceOverride(), + ...getModelServiceOverride(), + ...getFilesServiceOverride(), + ...getConfigurationServiceOverride(), + ...getLanguagesServiceOverride(), + ...getMonarchServiceOverride(), + ...getEditorServiceOverride((model, input, sideBySide) => { + // Single-editor mode: no-op for "open editor" requests + return undefined; + }), +}).then(() => { + console.log("[Monaco] VSCode services initialized (classic mode)"); +}); diff --git a/src/renderer/lib/monaco-luals-client.ts b/src/renderer/lib/monaco-luals-client.ts new file mode 100644 index 000000000..c98c9a187 --- /dev/null +++ b/src/renderer/lib/monaco-luals-client.ts @@ -0,0 +1,219 @@ +/** + * LuaLS client using MonacoLanguageClient. + * + * Connects to the LuaLS WebSocket bridge (ws://localhost:8089) spawned by + * the electron main process (ipcmain_luals.ts). MonacoLanguageClient + * auto-registers all LSP providers: completion, hover, signature help, + * diagnostics, semantic tokens, etc. + * + * Annotation library (grid-api.lua) is bundled via ?raw import and injected + * via textDocument/didOpen after the client starts — no filesystem access needed. + */ +import { MonacoLanguageClient } from "monaco-languageclient"; +import { + toSocket, + WebSocketMessageReader, + WebSocketMessageWriter, +} from "vscode-ws-jsonrpc"; +import { CloseAction, ErrorAction } from "vscode-languageclient/browser.js"; +import { Uri, editor as monacoEditorAPI } from "monaco-editor"; +import { + registerFileSystemOverlay, + FileSystemProviderCapabilities, + FileType, +} from "@codingame/monaco-vscode-files-service-override"; +import gridApiLua from "../../../build-assets/lua-annotations/grid-api.lua?raw"; + +// initFile for this URI is called in monaco-workers.ts, which runs before +// initialize() from @codingame/monaco-vscode-api marks services as initialized. +const ANNOTATIONS_URI = Uri.parse("file:///grid-annotations/grid-api.lua"); + +// Serve file:///grid-editor/ URIs from Monaco's model registry so the peek +// definition widget can read the current editor content without hitting disk. +// Priority -1 = fallback behind the default in-memory FS (priority 0). +registerFileSystemOverlay(-1, { + capabilities: + FileSystemProviderCapabilities.FileReadWrite | + FileSystemProviderCapabilities.Readonly, + onDidChangeCapabilities: () => ({ dispose: () => {} }), + onDidChangeFile: () => ({ dispose: () => {} }), + watch: () => ({ dispose: () => {} }), + stat: async (uri) => { + const model = monacoEditorAPI + .getModels() + .find((m) => m.uri.toString() === uri.toString()); + if (!model) throw new Error(`No model for ${uri}`); + return { type: FileType.File, ctime: 0, mtime: Date.now(), size: 0 }; + }, + readFile: async (uri) => { + const model = monacoEditorAPI + .getModels() + .find((m) => m.uri.toString() === uri.toString()); + if (!model) throw new Error(`No model for ${uri}`); + return new TextEncoder().encode(model.getValue()); + }, + writeFile: async () => { + throw new Error("read only"); + }, + mkdir: async () => {}, + readdir: async () => [], + delete: async () => {}, + rename: async () => {}, +}); + +const LUALS_WS_URL = "ws://localhost:8089"; + +const elementTypeToLuaClass: Record = { + button: "ButtonElement", + encoder: "EncoderElement", + potmeter: "PotmeterElement", + fader: "FaderElement", + endless: "EndlessElement", + lcd: "LCDElement", + system: "SystemElement", +}; + +let contextCounter = 0; + +const luaConfig = { + runtime: { version: "Lua 5.4" }, + diagnostics: { + globals: ["self", "element"], + severity: { "undefined-global": "Warning" }, + }, + completion: { callSnippet: "Replace" }, +}; + +let client: MonacoLanguageClient | null = null; +let webSocket: WebSocket | null = null; + +/** + * Start the MonacoLanguageClient connected to the LuaLS bridge. + * Safe to call multiple times — will no-op if already running. + */ +export async function startLuaLSClient(): Promise { + if (client) return; + + return new Promise((resolve, reject) => { + const ws = new WebSocket(LUALS_WS_URL); + webSocket = ws; + + ws.addEventListener("error", (ev) => { + console.error("[LuaLS] WebSocket error:", ev); + reject(new Error("WebSocket connection failed")); + }); + + ws.addEventListener("open", async () => { + const socket = toSocket(ws); + const reader = new WebSocketMessageReader(socket); + const writer = new WebSocketMessageWriter(socket); + + client = new MonacoLanguageClient({ + name: "LuaLS", + id: "lua", + clientOptions: { + documentSelector: [{ language: "intech_lua" }], + errorHandler: { + error: () => ({ action: ErrorAction.Continue }), + closed: () => ({ action: CloseAction.DoNotRestart }), + }, + // Respond to workspace/configuration pulls from LuaLS. + // callSnippet: "Replace" tells LuaLS to insert function calls as + // snippets with parameter placeholders. + middleware: { + workspace: { + configuration: async (params, token, next) => { + await next(params, token); + return (params.items ?? []).map(() => luaConfig); + }, + }, + // Only allow definitions that point to our registered virtual + // annotation file (file:///grid-annotations/...). Everything else + // — LuaLS meta files, per-editor context stubs — is not registered + // in the in-memory filesystem and would throw FileOperationError + // when the peek widget tries to open them. + provideDefinition: async (model, position, token, next) => { + const result = await next(model, position, token); + if (!result) return result; + const locations = Array.isArray(result) ? result : [result]; + const filtered = locations.filter((loc: any) => { + const uri = + loc.uri?.toString() ?? loc.targetUri?.toString() ?? ""; + return uri.startsWith("file:///grid-annotations/"); + }); + return filtered.length > 0 ? filtered : null; + }, + }, + }, + messageTransports: { reader, writer }, + }); + + try { + await client.start(); + await client.sendNotification("workspace/didChangeConfiguration", { + settings: { Lua: luaConfig }, + }); + await client.sendNotification("textDocument/didOpen", { + textDocument: { + uri: ANNOTATIONS_URI.toString(), + languageId: "lua", + version: 1, + text: gridApiLua, + }, + }); + console.log("[LuaLS] MonacoLanguageClient started"); + resolve(); + } catch (err) { + console.error("[LuaLS] Client start error:", err); + client = null; + reject(err); + } + }); + }); +} + +/** + * Open a per-editor context document that types `self`, `element`, and `ele` + * as the specific element subclass for this editor instance. + * Returns the URI of the context document, to be passed to closeEditorContext on destroy. + */ +export async function openEditorContext( + elementType: string, +): Promise { + if (!client) return null; + const className = elementTypeToLuaClass[elementType] ?? "Element"; + const uriString = `file:///grid-context/editor-${++contextCounter}.lua`; + const text = `---@type ${className}\nself = {}\n---@type ${className}[]\nelement = {}\n---@type ${className}[]\nele = {}\n`; + await client.sendNotification("textDocument/didOpen", { + textDocument: { uri: uriString, languageId: "lua", version: 1, text }, + }); + return uriString; +} + +/** + * Close the per-editor context document when the editor is destroyed. + */ +export async function closeEditorContext(uri: string): Promise { + if (!client) return; + await client.sendNotification("textDocument/didClose", { + textDocument: { uri }, + }); +} + +/** + * Stop the client and close the WebSocket. + */ +export async function stopLuaLSClient(): Promise { + if (client) { + try { + await client.stop(); + } catch { + // ignore stop errors + } + client = null; + } + if (webSocket) { + webSocket.close(); + webSocket = null; + } +} diff --git a/src/renderer/lib/monaco-workers.ts b/src/renderer/lib/monaco-workers.ts index f0af78952..992c2b1eb 100644 --- a/src/renderer/lib/monaco-workers.ts +++ b/src/renderer/lib/monaco-workers.ts @@ -1,18 +1,31 @@ /** - * Monaco Editor worker setup for Vite (no plugin required). + * Monaco Editor worker setup for @codingame/monaco-vscode-api. * - * Uses Vite's built-in `?worker` import to bundle the base editor worker. - * Only the generic editor worker is needed — Lua/intech_lua use Monarch - * tokenizers which run on the main thread. + * Vite's ?worker suffix bundles the entry point into a self-contained file + * and gives us a Worker constructor. We set MonacoEnvironment.getWorker so + * StandaloneWebWorkerService uses it directly — bypassing the blob-URL + * bootstrap that can't resolve bare specifiers under Electron's file:// + * protocol. * - * When migrating to LuaLS + monaco-languageclient in the future, - * the language server will communicate via WebSocket/IPC, - * not through Monaco's built-in worker system. + * This must run before initialize() from @codingame/monaco-vscode-api. */ -import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; +import { useWorkerFactory } from "monaco-languageclient/workerFactory"; +import EditorWorker from "@codingame/monaco-vscode-editor-api/esm/vs/editor/editor.worker.js?worker"; +import { Uri } from "monaco-editor"; +import { initFile } from "@codingame/monaco-vscode-files-service-override"; +import gridApiLua from "../../../build-assets/lua-annotations/grid-api.lua?raw"; -self.MonacoEnvironment = { - getWorker() { - return new editorWorker(); - }, -}; +// Pre-populate the annotation file in the in-memory filesystem BEFORE initialize() +// from @codingame/monaco-vscode-api runs (which happens in monaco-init.ts after this +// import). initFile called after initialize() throws "Services are already initialized". +initFile(Uri.parse("file:///grid-annotations/grid-api.lua"), gridApiLua); + +// useWorkerFactory initialises MonacoEnvironment (viewServiceType, etc.) +useWorkerFactory({ workerLoaders: {} }); + +// getWorker is checked BEFORE getWorkerUrl in StandaloneWebWorkerService. +// Returning a real Worker bypasses the blob wrapper entirely. +const monacoEnv = (self as any).MonacoEnvironment; +if (monacoEnv) { + monacoEnv.getWorker = () => new EditorWorker(); +} diff --git a/src/renderer/lib/monaco.ts b/src/renderer/lib/monaco.ts index fcddcd00a..43f07d1b7 100644 --- a/src/renderer/lib/monaco.ts +++ b/src/renderer/lib/monaco.ts @@ -1,15 +1,16 @@ -// Worker setup — must be imported before any monaco-editor usage -import "./monaco-workers"; +// Service initialization — must be imported before any monaco-editor usage. +// monacoReady resolves once all VSCode services are set up. +import { monacoReady } from "./monaco-init"; import { editor as monaco_editor, languages as monaco_languages, Position, + Range, } from "monaco-editor"; import { TabFocus } from "monaco-editor/esm/vs/editor/browser/config/tabFocus.js"; import { ElementType, grid } from "@intechstudio/grid-protocol"; - -let hoverTips = {}; +import { startLuaLSClient, stopLuaLSClient } from "./monaco-luals-client"; const language_config: monaco_languages.LanguageConfiguration = { comments: { @@ -37,7 +38,7 @@ const language_config: monaco_languages.LanguageConfiguration = { ], }; -const [lua, intech_lua] = Array(2).fill({ +const createLangDef = () => ({ defaultToken: "", tokenPostfix: ".lua", keywords: [ @@ -70,34 +71,35 @@ const [lua, intech_lua] = Array(2).fill({ "acos", "asin", "atan", - "atan2", "ceil", "cos", - "cosh", "deg", "exp", "floor", "fmod", - "frexp", - "huge", - "ldexp", "log", - "log10", "max", "min", "modf", - "pi", - "pow", "rad", "random", "randomseed", "sin", - "sinh", "sqrt", "tan", - "tanh", + "tointeger", + "type", + "ult", + ], + variables: [ + "self", + "element", + "math", + "huge", + "maxinteger", + "mininteger", + "pi", ], - variables: ["self", "element", "math"], forbiddens: [], brackets: [ { token: "delimiter.bracket", open: "{", close: "}" }, @@ -125,6 +127,12 @@ const [lua, intech_lua] = Array(2).fill({ ".", "..", "...", + "//", + "&", + "|", + "~", + "<<", + ">>", ], symbols: /[=> e.label === element)) { - continue; - } - - let proposalItem = { - kind: monaco_languages.CompletionItemKind.Function, - documentation: "Documentation", - range: range, - label: element, - insertText: element + "()", - }; - - proposalList.push(proposalItem); - } - - return proposalList; - } - - function createIntechLuaProposals( - range: Range, - model: monaco_editor.ITextModel, - position: Position, - prefix: string, - ) { - const instance: MonacoEditor.CustomCodeEditor = monaco_editor - .getEditors() - .find((e) => e.getModel().uri === model.uri); - - const scope = - instance.restrictScope === ElementType.FADER - ? ElementType.POTMETER - : instance.restrictScope; - - let proposalList = []; - intech_lua.functions = ["print"]; - - // Handle other general cases (mathfunctions, keywords, etc.) - for (const element of intech_lua.mathfunctions) { - let proposalItem = { - label: "", - kind: monaco_languages.CompletionItemKind.Function, - documentation: "Documentation", - insertText: "", - range: range, - }; - - proposalItem.label = "math." + element; - proposalItem.insertText = "math." + element; - proposalList.push(proposalItem); - } - - for (const element of intech_lua.keywords) { - let proposalItem = { - label: "", - kind: monaco_languages.CompletionItemKind.Keyword, - documentation: "Documentation", - insertText: "", - range: range, - }; - - proposalItem.label = element; - proposalItem.insertText = element; - - proposalList.push(proposalItem); - } - - for (const item of grid.lua_function_to_human_map()) { - let proposalItem = { - label: "", - kind: monaco_languages.CompletionItemKind.Function, - documentation: "Documentation", - insertText: "", - range: range, - }; - - const key = item[0]; - const value = item[1]; - const elementTypeMapping = { - GRID_LUA_FNC_EP: "endless", - GRID_LUA_FNC_E: "encoder", - GRID_LUA_FNC_B: "button", - GRID_LUA_FNC_P: "potmeter", - GRID_LUA_FNC_L: "lcd", - }; - - const lineContent = model.getLineContent(range.startLineNumber); - const isInsideSelfOrElement = - lineContent.includes("self:") || lineContent.includes("element["); - const keyPrefix = Object.keys(elementTypeMapping).find((prefix) => - key.startsWith(prefix), - ); - - if ( - keyPrefix && - (scope === elementTypeMapping[keyPrefix] || scope === undefined) - ) { - proposalItem.label = isInsideSelfOrElement ? value : `self:${value}`; - proposalItem.insertText = `${proposalItem.label}()`; - } else if (scope === ElementType.SYSTEM || !scope) { - if (!proposalList.some((e) => e.label === `element[0]:${value}`)) { - proposalItem.label = isInsideSelfOrElement - ? value - : `element[0]:${value}`; - proposalItem.insertText = `${proposalItem.label}()`; - } - } else if (!keyPrefix) { - proposalItem.label = value; - proposalItem.insertText = `${value}()`; - } - - // Only push items that were actually populated — Monaco 0.55+ - // rejects completion items with empty labels. - if (proposalItem.label !== "") { - proposalList.push(proposalItem); - } - - const helperText = grid.get_lua_function_helper(key); - if (typeof helperText !== "undefined") { - hoverTips[value] = helperText; - } - } - - for (const element of intech_lua.functions) { - if (proposalList.find((e) => e.label === element)) { - continue; - } - - let proposalItem = { - kind: monaco_languages.CompletionItemKind.Function, - documentation: "Documentation", - range: range, - label: element, - insertText: element + "()", - }; - - proposalList.push(proposalItem); - } - - return proposalList; - } - - monaco_languages.registerCompletionItemProvider("intech_lua", { - provideCompletionItems: function (model, position) { - const word = model.getWordUntilPosition(position); - const lineContent = model.getLineContent(position.lineNumber); - const range: Range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn, - }; - - // Check for 'self:' or 'element[x]:' - const selfIndex = lineContent.lastIndexOf("self:", position.column - 1); - const elementIndex = lineContent.lastIndexOf( - "element[", - position.column - 1, - ); - - // If 'self:' or 'element[x]:' is found, adjust the prefix - let prefix = ""; - if (selfIndex !== -1 && selfIndex + 5 <= word.startColumn) { - prefix = "self:"; - } else if (elementIndex !== -1 && elementIndex + 8 <= word.startColumn) { - prefix = lineContent.slice(elementIndex, position.column); // 'element[x]:' - } - - return { - suggestions: createIntechLuaProposals(range, model, position, prefix), - }; - }, - }); - - monaco_languages.registerCompletionItemProvider("lua", { - provideCompletionItems: function (model, position) { - const word = model.getWordUntilPosition(position); - const range: Range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn, - }; - - return { - suggestions: createLuaProposals(range), - }; - }, - }); -} +// Old hand-rolled completion providers removed. +// All completion/hover/signature/diagnostics now handled by MonacoLanguageClient via LuaLS. function initialize_highlight() { grid.lua_function_to_human_map().forEach((value, key) => { @@ -495,26 +275,13 @@ function initialize_highlight() { }); } -function initialize_hover() { - monaco_languages.registerHoverProvider("intech_lua", { - provideHover: function (model, position) { - if (model.getWordAtPosition(position) !== null) { - const word = model.getWordAtPosition(position).word; - - if (hoverTips[word] !== undefined) - return { - contents: [ - { value: "**SOURCE**" }, - { value: "```html\n" + hoverTips[word] + "\n```" }, - ], - }; - } - }, - }); -} +// Old hand-rolled hover provider removed. +// Hover is now handled by MonacoLanguageClient via LuaLS. function initialize_grammar() { - monaco_languages.setMonarchTokensProvider("intech_lua", intech_lua); + // Cast needed: @codingame/monaco-vscode-editor-api has stricter IMonarchLanguage + // types than standalone monaco-editor, but the tokenizer definition is valid. + monaco_languages.setMonarchTokensProvider("intech_lua", intech_lua as any); monaco_languages.setLanguageConfiguration("intech_lua", language_config); } @@ -523,12 +290,23 @@ export namespace MonacoEditor { LIGHT = "monaco-light", DARK = "monaco-dark", } - initialize_theme(); - initialize_language(); - initialize_highlight(); - initialize_autocomplete(); - initialize_hover(); - initialize_grammar(); + + // Initialization is deferred until monacoReady resolves. + // The ready promise is exposed so consumers can await it before creating editors. + export const ready: Promise = monacoReady.then(() => { + initialize_theme(); + initialize_language(); + initialize_highlight(); + initialize_grammar(); + + // Connect to LuaLS via MonacoLanguageClient (non-blocking, logs on failure) + startLuaLSClient().catch((err) => + console.warn( + "[LuaLS] Client start failed (server may not be running):", + err, + ), + ); + }); export type Options = monaco_editor.IStandaloneEditorConstructionOptions; @@ -545,6 +323,9 @@ export namespace MonacoEditor { ) { const editor: CustomCodeEditor = monaco_editor.create(node, { ...options, + // Enable semantic highlighting so LuaLS semantic tokens (function colors, + // global variable colors, etc.) are applied on top of Monarch tokenizer. + "semanticHighlighting.enabled": true, }); editor.restrictScope = options.restrictScope; diff --git a/src/renderer/main.js b/src/renderer/main.js index 675a00829..c086e9f1c 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -10,15 +10,20 @@ async function initApp() { try { // Initialize the Lua formatter WASM module await initLuaFormatter(); + } catch (err) { + console.warn( + "Lua formatter WASM initialization failed, formatting features will be unavailable:", + err, + ); + } - // Initialize the config block registry (built-in blocks are eager, package component is lazy) - await init_config_block_library(); + // Initialize the config block registry (built-in blocks are eager, package component is lazy) + await init_config_block_library(); - // Initialize the Svelte app after the configuration is ready - app = mount(App, { - target: document.body, - }); - } catch (err) {} + // Initialize the Svelte app after the configuration is ready + app = mount(App, { + target: document.body, + }); } // Call the function to initialize the app diff --git a/src/renderer/main/ErrorConsole.svelte b/src/renderer/main/ErrorConsole.svelte index 5ca359e46..11a4687aa 100644 --- a/src/renderer/main/ErrorConsole.svelte +++ b/src/renderer/main/ErrorConsole.svelte @@ -3,6 +3,7 @@ import { fly, fade, slide } from "svelte/transition"; import { Analytics } from "../runtime/analytics.js"; + import { copyContextMenu } from "./_actions/copy-context-menu.action.js"; const ctxProcess = window.ctxProcess; const configuration = ctxProcess.configuration(); @@ -236,7 +237,7 @@ ? 'bg-gray-800' : 'bg-gray-900'} justify-center flex flex-row items-center h-16" > -
{log.reason}
+
{log.reason}
{#if log.solution !== undefined}
{log.solution.message}
@@ -281,7 +282,7 @@ ? notification.class : 'bg-green-500'} justify-center flex flex-row items-center h-16" > -
{notification.message}
+
{notification.message}
{#if notification.link !== undefined && notification.link !== ""}