Skip to content

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

Closed
rubensworks wants to merge 10 commits into
master-1.21-ltsfrom
terminal-ux/2-undo-and-sync
Closed

rubensworks wants to merge 10 commits into
master-1.21-ltsfrom
terminal-ux/2-undo-and-sync

Conversation

@rubensworks

@rubensworks rubensworks commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Phase 2 of the scripting terminal UX improvements, stacked on #77, so review and merge that one first. Each commit fixes one problem.

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. New unit test that saves a 1 MB script 20 times while the watcher runs. It fails on the old code with partial contents.
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.
Keep file permissions when writing scripts atomically The atomic write used Files.createTempFile, which creates files readable only by the owner, so flushed scripts became rw-------. New unit test that fails without the fix (rw-------) and passes with it: new files get rw-r--r--, and existing files keep their permissions.
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: the terminal list matches the disk right after the same batch, and after adding files to an empty and an intermediate folder.

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 (17 tests) had no failures. There are tests for the reload during the first flush (fails on the old code), and for external modifications and deletions still being applied.

This race is also a plausible cause of the "empty script after an external write" case I saw once earlier, but I didn't reproduce that again to confirm.

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. With the atomic write, all four test files came out exactly hello world.

Testing

  • ./gradlew build passes, including the new unit tests.
  • ./gradlew runGameTestServer: all 22 tests pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9

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.

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
@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 39131475676

Coverage decreased (-0.3%) to 46.454%

Details

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

Uncovered Changes

No uncovered changes found.

Coverage Regressions

481 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 132 0.0%
org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java 64 26.67%
org/cyclops/integratedscripting/core/network/ScriptingData.java 41 78.95%
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: 2761
Covered Lines: 1405
Line Coverage: 50.89%
Relevant Branches: 976
Covered Branches: 331
Branch Coverage: 33.91%
Branches in Coverage %: Yes
Coverage Strength: 23.27 hits per line

💛 - Coveralls

Temporary files are created with owner-only permissions, so writing
scripts through a temporary file made them unreadable for other users,
for example the group of a server's file manager. Use the permissions of
the existing file, or those of a regular new file.

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
Base automatically changed from terminal-ux/1-editor-fixes to master-1.21-lts October 18, 2026 15:19

Copy link
Copy Markdown
Member Author

Replaced by #85: same changes, rebased onto master-1.21-lts with every commit as one standalone change.


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.

2 participants