Skip to content

Add undo/redo and fix lost edits in the scripting terminal - #85

Merged
rubensworks merged 6 commits into
master-1.21-ltsfrom
terminal-ux-clean/2-undo-and-sync
Oct 18, 2026
Merged

rubensworks merged 6 commits into
master-1.21-ltsfrom
terminal-ux-clean/2-undo-and-sync

Conversation

@rubensworks

Copy link
Copy Markdown
Member

Replaces #78, with the same changes and a clean commit history: every commit is one standalone change. Phase 2 of the scripting terminal UX improvements, and the base of the remaining stack.

I reproduced every problem in a dev client driven by clientdevbridge (or with a unit test that fails on the old code) before fixing it, then checked it again with this branch.

Commit Problem before Result with this PR
Add undo and redo to the script editor A right-click selected the whole 40-line script; one key press replaced it, and that was saved to disk (z). No undo existed. Right-click no longer selects everything. Ctrl+Z undoes and Ctrl+Y / Ctrl+Shift+Z redo, with consecutive typing grouped into one step (checked: abc, Enter, de undo as 3 steps). History is kept per file. Unit tests added.
Don't echo synced scripts back to the client The server sent each synced script back. A keystroke typed between the sync and the echo was overwritten: the editor showed aAB but aA was saved. aAB is saved.
Write scripts to disk atomically The server's file watcher could reload a half-written file into memory while the server was writing it. Scripts are written to a temporary file and moved into place. A new unit test saves a 1 MB script 20 times while the watcher runs; it fails on the old code with partial contents. Existing files keep their permissions and new files get rw-r--r-- (a first version used Files.createTempFile, which made flushed scripts rw-------; covered by a test).
Show external script changes in the open editor After editing the file outside the game, the open editor kept the old text, and the next keystroke wiped the external change from disk. The editor reloads the new text as an undoable step and keeps the cursor. The external change survives further typing.
Only apply external changes when the file watcher reloads scripts The watcher also fires on the server's own writes, and its reload replaced everything in memory with what was on disk at that moment. Right after creating a new disk's folder, before the file was written, this made the first script disappear. Two concurrent reloads could also overwrite each other. The server remembers what it last wrote or read per file, and applies only files that changed outside the mod. Reloads, flushes and script changes now take a shared lock.
Notice all externally created script files Files created outside the game didn't show up until some other file changed. Each reload cancelled and re-registered the folder watchers, losing events in between, and only folders that already contained scripts were watched. Reproduced in-game: deleting 4 scripts and creating 3 new files (one in a new lib/helpers/ folder) in one shell command left the terminal showing the old list. All folders of a disk are watched, including empty and intermediate ones, before its files are read; watchers are no longer re-registered; deleted folders are cleaned up. New tests (deletes followed by creates; files in new nested, intermediate and empty folders) fail 3/3 on the old code and pass now. Rechecked in-game.

About the watcher reload commit

I found it because the partial-contents test was flaky: run 15 times in one JVM, it failed 13 times, always observing the script as null. That was this race, not partial contents. After both watcher commits, 30 runs of the whole ScriptingDataTest class had no failures.

Why the atomic write commit is needed

Without it, the external-changes commit caused a regression: typing hello world into a new file gave orld, o worldllhe and rldhello wo, because the editor applied the half-written content that the watcher had read back.

Testing

  • Every commit compiles on its own, including tests and Spotless.
  • ./gradlew build passes, including the new unit tests.
  • ./gradlew runGameTestServer: all 23 tests pass.

Generated by Claude Code

Scripts are saved on every edit and right-click selected the whole
script, so a right-click followed by a single key press replaced and
saved the entire script, without any way to get it back.

Add a per-file undo history (ctrl+z, and ctrl+y or ctrl+shift+z to redo)
that groups consecutive typing, and stop selecting everything on
right-click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9
When the client synced a script, the server sent the same version back.
If the player typed in the meantime, this echo overwrote the newer
client version, which was then synced instead, losing the last
keystrokes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9
Scripts were flushed by truncating and rewriting the file, so the file
watcher could reload a partially written or empty script into memory and
send it to clients. Write to a temporary file outside the watched
directories and move it into place instead, keeping the permissions of
existing files and the default permissions for new ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9
When a script changed outside the editor, for example by editing the
file on disk, the open editor kept showing the old text, and the next
key press synced that old text back, discarding the external change.

Reload the active script when the server sends a newer version, as an
undoable edit that keeps the cursor position.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9
When the file watcher reloaded a disk directory, it replaced the scripts
in memory with whatever was on disk at that moment. As it also triggers
on the server's own writes, it could run while a script was not written
yet, for example right after creating the directory of a new disk, which
made that script disappear. Concurrent reloads could also overwrite each
other.

Remember the contents last written or read for each file, only apply
files that changed outside of the mod, and don't let reloads, flushes and
script changes run at the same time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9
Files created outside of the game did not always show up until another
file changed. Every reload cancelled and re-registered the directory
watchers, losing events that happened in between, and only directories
that already contained scripts were watched, so files in new, empty or
intermediate directories were missed.

Watch all directories of a disk before reading its files, keep existing
watchers registered, and clean up watchers of deleted directories.

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

Copy link
Copy Markdown

Coverage Report for CI Build 39145586285

Coverage decreased (-0.5%) to 47.629%

Details

  • Coverage decreased (-0.5%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 483 coverage regressions across 8 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

483 previously-covered lines in 8 files lost coverage.

File Lines Losing Coverage Coverage
org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java 215 0.0%
org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java 143 0.0%
org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java 56 45.9%
org/cyclops/integratedscripting/core/network/ScriptingData.java 40 79.24%
org/cyclops/integratedscripting/client/gui/component/input/TextFieldHelperExtended.java 17 0.0%
org/cyclops/integratedscripting/core/network/GraalScriptFactory.java 5 60.53%
org/cyclops/integratedscripting/network/packet/TerminalScriptingModifiedScriptPacket.java 4 0.0%
org/cyclops/integratedscripting/core/evaluate/ScriptVariable.java 3 81.25%

Coverage Stats

Coverage Status
Relevant Lines: 2815
Covered Lines: 1463
Line Coverage: 51.97%
Relevant Branches: 1002
Covered Branches: 355
Branch Coverage: 35.43%
Branches in Coverage %: Yes
Coverage Strength: 24.64 hits per line

💛 - Coveralls

@rubensworks
rubensworks merged commit 5a82006 into master-1.21-lts Oct 18, 2026
23 checks passed
@rubensworks
rubensworks deleted the terminal-ux-clean/2-undo-and-sync branch October 18, 2026 17:46
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.

2 participants