Skip to content

Edit Logic Programmer expressions as text - #1767

Merged
rubensworks merged 5 commits into
feature/expression-cardsfrom
feature/expression-text
Oct 17, 2026
Merged

rubensworks merged 5 commits into
feature/expression-cardsfrom
feature/expression-text

Conversation

@rubensworks

@rubensworks rubensworks commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Stacked on #1766 (feature/expression-cards), so this PR only shows the commits on top of it. It should be merged after that one, or retargeted to master-1.21-lts once that one is in.

This is a first implementation of editing an expression in the Logic Programmer as text, and of copying it as text. The panel gets a row of regular buttons at the bottom:

  • Edit turns the expression into text, for example (a: integer, b: integer) -> (a + b) * #5. While you type, the client parses and type checks the text and shows the inferred type in the colors of its types, or the first error with its line and column (Line 1, column 24: The text ends too early, a value is missing). Apply (or Ctrl+Enter) applies it and is disabled while the text does not parse; Cancel (or Esc) leaves without changes.
  • Copy puts that text on the clipboard.
  • Undo goes back to the expression from before the last Apply. Clicking it again redoes.

Screenshots from the dev client: https://claude.ai/artifact/JKSFeHqKTEyB3H72wCD1tc

Design

Typing never waits on the server. Parsing, name resolution and type checking all run on the client, against the ref types the editor knows plus the cards in the player's inventory. Nothing is sent while typing. On Apply, the client applies the result locally and sends the tree once, as a new set action in the existing LogicProgrammerExpressionActionPacket. The server deserializes it with the existing limits (depth 64, 8 parameters, known operators and types) and also checks ExpressionEditor.isEditable: the root is an operator, at most 1024 nodes, parameter names that the text form can read back, parameter types from the LP's list, and only operators that have a slot for each input. Anything else is ignored.

Another root operator switches the element. Each LP element is one root operator, so if the text has another root (typing (a + b) * 2 while in >), the element of that operator becomes active, the same way loading a card does. The known ref types and the actual cards placed in slots are carried over first, since deactivating the old element forgets them.

Text format. It reuses the naming from #1766, so it reads like Integrated Scripting.

  • Calls use the scoped name when the type of the first argument tells it apart (length(s) for strings, length(l) for lists), and otherwise the global one (stringLength). Both are accepted when parsing.
  • Infix + - * / % == != < <= > >= && || with C precedence, prefix !, and c ? a : b.
  • ? is an empty slot, and (name) is an operator as a value, such as (+) or (stringLength).
  • Parameters are declared in front: x -> ..., or (x: integer, y) -> ... with optional types.
  • Literals are true/false, 1, 5L, 1.5/1e3/2d/NaN/Infinity, and quoted strings with \" \\ \n \t \r \uXXXX escapes.
  • Any other value is written as its type with its serialized form, @type{snbt}, so the printer is exact for every value, not only the common ones.
  • #42 refers to a card by ID. #limit or #"my label" refers to it by label, and only resolves when exactly one card has that label. Copy prints #label only in that case, and #id otherwise.

ExpressionPrinter.printExact is the inverse of ExpressionParser.parse, and the tests check this both ways (see Testing).

What changed

  • New ExpressionParser (recursive descent with a small lexer, max 8192 characters) and ExpressionParseException (position and translatable message). 28 error messages, all translatable.
  • ExpressionPrinter.printExact, printLiteral and quote. The display form from Expression cards in the Logic Programmer, editable as text #1766 is unchanged.
  • OperatorNames: getInfixOperator, getPrefixOperator and getScopedNameOperators for parsing, and isRegistered is now public.
  • ExpressionEditor: ACTION_SET with actionSet/readSetAction, isEditable(IOperator) (extracted from the existing op action check) and isEditable(Expression), getParserContext, getKnownRefTypes. Renaming a parameter now also rejects names the text could not read back, such as true, NaN or 1x.
  • OperatorLPElement: setExpression, getReferences/addReferences, getInventoryCardTypes.
  • ContainerLogicProgrammerBase: applySetExpressionAction and getOperatorElement(IOperator), plus client-side state for the text being edited and the undo. That state lives in the container, so it survives the panel being recreated.
  • LabelsWorldStorage.getVariableIds(label).
  • ContainerScreenLogicProgrammerBase also forwards scrolling to the panel (dragging and releasing already are, for dragging parameters in Expression cards in the Logic Programmer, editable as text #1766), since CyclopsCore sub-GUIs only get clicks and keys and the text box needs them for scrolling and selecting.
  • In text mode, the text box ends right above the footer from Expression cards in the Logic Programmer, editable as text #1766, which then shows the type of the typed text, or the parse error with its line and column.
  • The buttons are CyclopsCore ButtonText widgets in the render pattern's buttonList, like the + button from Expression cards in the Logic Programmer, editable as text #1766, laid out right-aligned in a row at the bottom of the panel with the main action on the right. Copy shows "Copied" for a moment after a click.
  • Book: one paragraph on editing as text, on the Expressions page.

Things to decide on

  • Typed references. In Expression cards in the Logic Programmer, editable as text #1766, refs only came from cards placed in slots, or from a card that was loaded. With text, you can type #123 for any ID. Evaluation still needs that variable in the same network, so I don't think this gives access to anything new, but it does relax the "refs only come from cards" rule I described in Expression cards in the Logic Programmer, editable as text #1766, so you should decide on it explicitly. Requiring the card in the inventory for every #id on the server would be easy to add, but it would also reject loading and re-applying a card whose referenced cards are elsewhere.
  • The text box is vanilla MultiLineEditBox: black, full-size font, about 23 characters per line in the 138 px panel. It is readable, and long expressions wrap and scroll, but there is no syntax highlighting, completion or error underline yet (only line and column). These are the obvious next steps if you want to take this further.
  • Undo is one step, client-side only, and is dropped as soon as the expression changes in any other way (a slot edit, a parameter, a reset), so it can never throw away a later visual edit. It is gone when the GUI closes.

Testing

  • Unit tests: 973 pass, including 12 new in TestExpressionParser and 2 new in TestExpressionEditor.

    • Every editable operator (more than 250) prints and parses back to itself.
    • The display form from Expression cards in the Logic Programmer, editable as text #1766 also parses.
    • Values, including NBT and a curried operator, and refs and labels print and parse back.
    • Precedence, parameters, type-dependent names, literals and errors with positions.
    • Rejecting what cannot be edited, and depth and node limits.
    • Breaking the L suffix on purpose makes two of them fail.
  • Gametests: all 1053 pass, including the new testLogicProgrammerSetExpressionAsText. It applies (#id + 3) * 2 to a real ContainerLogicProgrammer that is on >, checks that Arithmetic Multiplication became active with that expression, and that a const root is ignored. It then writes the card and checks that a display panel shows 36.

  • In the dev client (screenshots above, from the current version), I checked:

    • loading a card and opening Edit
    • errors with line and column while typing, with Apply disabled
    • the message of an error of an operator as a whole (b ? 1 : "a"), cut off in the footer and complete on hover
    • the colored type of the typed text (Integer -> String -> String, Boolean -> Integer)
    • Esc cancelling without closing the GUI, and typing e not closing it either
    • Apply with a different root, which switched the element, showed the real #5 card in its slot and rewrote the card
    • Copy giving the exact text, with the button's hover, tooltip and "Copied"
    • Undo restoring the previous root and card
    • a visual edit hiding Undo
    • scoped names (length, concat) and string literals
    • 8 parameters with 16-character names, in both modes (the text box scrolls)
    • dragging a parameter onto a slot, and selecting text in the box by dragging, which share the forwarded mouse events

    The vanilla box's own character counter overlapped the type, so it is turned off; the parser enforces the length limit instead. Reviewing the code also showed that the undo survived later visual edits, so Undo could throw them away. That is fixed, and the "visual edit hiding Undo" check covers it.

  • Not exercised in the client: Ctrl+Enter and paste. clientdevbridge sends modifier bits, but vanilla checks the physical key state with Screen.hasControlDown(), so held Ctrl does not reach the GUI. I used the Apply button instead. Ctrl+Enter uses the same hasControlDown() check as vanilla's own shortcuts, and paste is vanilla MultiLineEditBox behavior.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y

The expression panel gets Edit, Copy and Undo buttons. Edit turns the
expression into text in the format of Integrated Scripting, which is
parsed and type checked on the client while typing. Applying sends the
resulting tree to the server once, as a single set action that the
server validates, and makes the element of the new root operator active
when it changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
@coveralls

coveralls commented Oct 17, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 48.735% (+0.5%) from 48.229% — feature/expression-text into feature/expression-cards

Brings in the simplified expression panel. Edit, Copy, Undo, Apply and
Cancel are now regular buttons in a row at the bottom of the panel, like
the other buttons of the Logic Programmer, instead of drawn boxes in the
title. The hint below the text box is gone with the other hints, and the
type of the typed text is shown in the colors of its types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The text box ends right above the footer of the panel, which shows the
type of the typed text, or the parse error with its line and column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
@rubensworks
rubensworks merged commit ee24343 into feature/expression-cards Oct 17, 2026
9 checks passed

Copy link
Copy Markdown
Member Author

Combined into #1766: its branch was fast-forwarded to include these commits and those of #1768, so this PR is closed.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants