Skip to content

Fix undo B - Ids on undo changes, rewordings and split of UndoManager, minor clarifications - #16680

Open
calixtus wants to merge 26 commits into
mainfrom
fix-undoredomore
Open

Fix undo B - Ids on undo changes, rewordings and split of UndoManager, minor clarifications#16680
calixtus wants to merge 26 commits into
mainfrom
fix-undoredomore

Conversation

@calixtus

@calixtus calixtus commented Aug 25, 2026

Copy link
Copy Markdown
Member

Summary

Second PR of the undo/redo series. this one fixes what reviewing that model turned up, and finishes the separation it started.

  • hasChanged() compared a net edit balance against the balance recorded at save time, so two different histories reached the same number: edit, save, undo, edit again, and the library reported itself unmodified although it held work that was never saved. Since that flag decides whether closing a library asks to save, the library closed silently and the second edit was lost. Positions are now identified rather than counted, so a saved position that history has discarded can never be matched again.

  • apply renamed in this iteration to applyEdit. performs the change and records it under one acquisition of the journal's monitor, closing the window in which an undo from another thread reverted the previous change while the new one stayed applied but unrecorded.

  • UndoManager becomes the recording interface that the ~118 classes editing the library depend on, JabRefUndoManager the implementation, and GuiUndoManager / JabRefGuiUndoManager the JavaFX layer above it (cp. CliPreferences / GuiPreferences).

  • No class type is threaded through the GUI any more: what a class asks for now says what it does with the journal.

  • Undo step names are now written for whoever will read them, taken from the action the user invoked where one exists, instead of string tokens like CHANGE_SELECTED_FIELD;

  • Two file operations that undo cannot reverse say so where the user decides, rather than leaving a dangling link to be discovered later.

      `jabref-contrib-policy:4.2:reviewed​:ok`
    

Steps to test

  1. The close prompt — open a library, edit an entry, save, press Ctrl+Z, then edit any field again. The tab title keeps its asterisk and closing the library asks whether to save. Before this PR it closed without asking and the edit was gone.
  2. Automatic field editor — open it on several entries, use Set, Append, Clear, Rename, Copy, Move and Swap in turn. Each is one undo step, and the "x / y affected entries" notification still reports the same counts as before.
  3. Linked files — delete a linked file from an entry: the dialog now states that undo does not restore files removed from disk. Rename one: the prompt states that undo restores the link, not the file name.
  4. Undo still groups as before — merge two entries, manage keywords, find and replace, generate citation keys, import several files at once, merge external changes: each remains a single undo step, and Undo/Redo enablement in the menu and toolbar follows the stacks as before.
  5. Search field — right-click in the search box and choose Undo: it still undoes typing in that box only, never a change to the library.

Still open issues for follow-up:

  • One journal for the whole application: undo in one library can undo an edit made in another
  • key accelerators not working
  • Saving one library reports another as unmodified, because both share the saved position
  • Undo Group operations not yet implemented
  • LibraryTab rolls back a failed Cut with a bare undo(), reverting whatever is on top instead
  • Keystroke-by-keystroke undo in text fields, and one queued FX refresh per edit
  • Partially applied change sets are logged, not shown to the user
  • Field editors offer no Undo item although Ctrl+Z works in them
  • Shared-database sync changes bypass the journal
  • No change model outside the GUI for jabkit dry-runs and jabsrv
  • The undo handle is threaded through sixteen classes that never call it. DI?
  • FieldChange should be converted to a record
  • Better names for all classes and records

Related issues and pull requests

Follow-up to #16627

AI usage

AIL3-AIL4

"Claude Code (model claude-opus-5-0)"

AI CHECKLIST.md walkthrough

1. Code self-review

Nullability and control flow

  • No == null / != null checks — JSpecify annotations used instead. Three Deque.peek() guards were replaced by isEmpty() + getFirst(). Two != null checks on ThreadLocal.get() remain in addEdit/applyEdit; the first is pre-existing and the second mirrors it deliberately, because a thread-local recorder has no annotated absence to express. Flagged rather than hidden.
  • No Objects.requireNonNull(...)
  • New classes annotated with @NullMarkedUndoManager, GuiUndoManager, JabRefGuiUndoManager
  • Optional consumed with ifPresent / map / orElseThrow
  • StringUtil.isBlank(...) where applicable — no new blank checks
  • No catch (Exception e) in added code — the one in notifyListeners predates this PR
  • No throw new RuntimeException(...) / IllegalStateException(...) added
  • Logged exceptions passed as the last logger argument

Style and idioms

  • [/] BibEntry withers — no new BibEntry construction
  • Modern Java — records, sealed types, List.of, switch patterns
  • [/] Precompiled Pattern — no regexes added
  • [/] BackgroundTask — no new threading
  • No commented-out code, no trivial comments, no AI-disclosure comments in source
  • Markdown Javadoc uses [Type] and backticks, no {@link} / {@code}

User-facing text

  • All user-facing text localized; the eight developer tokens that were reaching ChangeSet.name are gone
  • Sentence case, no trailing !, no label colons
  • Placeholders rather than concatenation — the two new strings take no arguments
  • [/] Security / XSS — no HTML response touched

Tests

  • Behaviour changes in logic covered — five tests added to JabRefUndoManagerTest, four of which fail without the corresponding fix (verified by reverting each)
  • Plain JUnit asserts, no @DisplayName, no swallowed exceptions
  • [/] Fetcher tests — none touched

2. Verification commands

  • ./gradlew :jabgui:test — 1009 tests, 7 failing, all failing identically on main in this environment (clipboard and TestFX window-focus tests, plus the known KeyBindingViewModelTest)
  • ./gradlew :jablib:test — 11034 tests, 1 failing: RemoteSetupTest.pingReturnsFalseForNoServerListening, which fails because a JabRef instance was running locally
  • ./gradlew checkstyleMain checkstyleTest — clean, all modules
  • ./gradlew modernizer — clean
  • ./gradlew --no-configuration-cache :rewriteDryRun — clean; it caught a logger still named after the renamed type, fixed in its own commit
  • ./gradlew javadoc — clean
  • [/] markdownlint — no Markdown changed

3. Documentation

  • CHANGELOG.md — two entries, one sentence each, under twenty words, end-user wording
  • Searched both issue trackers; no confident match, so TODO placeholders were kept
  • docs/requirements/ — bug fixes and refactors
  • Developer documentation under docs/ — architecture changed (the interface/implementation split); worth a look before merge

4. Pull request

  • Body built from the template, every section filled
  • All checklist items kept and marked
  • All HTML comments removed
  • Created with gh pr create --body-file — for the author to run
  • TODO placeholders in CHANGELOG.md replaced with the PR number after creation

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license
  • If AI tools were used, I disclosed them in the "AI usage" section and reviewed, understood, and take full ownership of all AI-generated code
  • [.] I manually tested my changes in running JabRef (always required)
  • I added JUnit tests for changes (if applicable)
  • [.] I added screenshots in the PR description (if change is visible to the user)
  • I added one sentence (max 20 words) to CHANGELOG.md describing the change from the user's point of view (if the change is visible to the user)
  • [.] I checked the user documentation for up to dateness and submitted a pull request to our user documentation repository

calixtus and others added 14 commits August 25, 2026 13:48
`hasChanged()` compared a net edit balance against the balance recorded
at save time. Two different histories reach the same balance, because
pushing a change clears the redo stack: save, undo, then make a
different edit, and the balance is back where it started although the
library now holds something that was never saved.

That is not only a stale modified marker. `hasChanged()` feeds
`LibraryTab.markChangedOrUnChanged()`, which sets `changedProperty`,
which `requestClose()` consults before offering to save — so the false
negative closes a library holding unsaved work without asking.

Positions are now identified rather than counted. Every push takes an
id from a counter that only ever increments, the id travels with the
change across both stacks so redo returns to the position it came from,
and `markUnchanged()` stores the id at the top of the undo stack.
Because ids are never reused, a saved position discarded by a
redo-stack clear or by the LIMIT trim can never be matched again, which
is the right answer in both cases: it is no longer reachable.

The id lives in a private `UndoJournalEntry` record rather than on
`BibChange`. A change is a value describing a modification; a position
in this journal is bookkeeping only the manager needs. Putting it on
the change would also cost `inverted()` its involution, since it would
have to either copy the id — giving two distinct positions one identity
— or drop it.

The empty stack needs its own id for the same reason: once a trim or a
`clear()` has discarded history for good, "undone back to nothing" is
no longer the state the library started in.

Three tests cover the defect and fail without this change. The two
existing trim tests pin the behaviour the id scheme had to preserve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
`apply()` was `change.apply(); addEdit(change);` — two separate
acquisitions of this object's monitor, with the model write outside it.
Between them the library holds the change and the journal does not, so
an undo arriving there reverts the *previous* change while the new one
stays applied but unrecorded. The history that ends up on the stack
then describes a library state that never existed.

The class header already stated the opposite as an invariant: "Applying
the change is inside the lock. It has to be: if the stack transition
and the model write could interleave, two threads could undo the same
change." `undo()` and `redo()` honour it; `apply()` was the one path
that did not, and commands do push from background tasks, so the two
threads the header describes exist.

The stack transition moves into a private `push()` that requires the
monitor, and `apply()` takes it once around both the model write and
the push. Applying foreign code under the lock is already the accepted
cost here — `undo()` does it — so this brings one path in line rather
than changing the design.

Inside an `addEdit` block there is no window to close: the recorder
belongs to one thread and nothing reaches the stacks until the block
ends, so that path takes no lock. `CompoundEdit.apply()` keeps its
two-step form for the same reason, and its javadoc now says so instead
of pointing at a lock invariant that does not apply to it.

The regression test asks the applying thread, from inside a `BibEntry`
subclass that probes on `setField`, whether it holds the journal's
monitor at that moment. Staging a second thread would only have shown
that it did not get in within some interval, which makes the assertion
a statement about a timeout; whether the lock is held is a fact
available on the spot, and no threads, executors or waits are needed to
read it. The probe overrides rather than subscribing to the entry's
field events, so the test adds no use of Guava's deprecated EventBus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
P15 stopped `apply()` from delegating to `addEdit()`: it has to hold the
monitor across the model write and the stack push, and `addEdit()` ends
by notifying listeners, which must never run under the lock. The branch
deciding whether a change reaches the stacks at all therefore had to be
restated in `apply()`, and the two methods came to read as near
duplicates of one another.

They are not duplicates. The only difference is who performs the
change: `addEdit` records one the caller has already made — the common
case, because the model hands the caller a `FieldChange` as a
by-product — while the other makes it and records it as one operation.
Everything after that is the same journal entry. `apply` said only that
it performed the change and left the recording to its javadoc, so the
pair is now `addEdit` / `applyEdit`: one verb apart, same noun, and the
javadoc's first sentence states the recording outright. `CompoundEdit`
carries the same pair, so code inside a recording block reads like code
outside one.

The empty-step guard the two share is one `isEmptyStep` helper rather
than the same condition written twice with two different comments, so
each method now reads as the same three-branch dispatch: hand to the
enclosing recorder, skip an empty step, or lock and push.

Renames only; no behaviour change, and the compiler enumerated all 28
call sites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
`AutomaticFieldEditorUndoableEdit` existed to add one `int` to
`CompoundEdit`, and `CompoundEdit` gave up `final` to allow it. The
subtype added no behaviour: the count is what the "Automatic field
editor" notification tells the user, and nothing in the undo model ever
read it — a value type carrying a field for the benefit of a dialog.

The count now travels as a parameter, `addEdit(CompoundEdit, int)`, to
the one method that consumed it. Every call site already had it in a
local variable, so the change removes a setter call rather than adding
an argument to compute. `AutomaticFieldEditorUndoableEdit` is deleted
and `CompoundEdit` is `final` again, leaving the change model with no
inheritance at all.

Recorded in the plan as the P11 follow-up, and promoted from "no
behavioural gain" by the SOLID review: it is the only place where
inheritance was standing in for composition in this area.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
Roughly 120 classes hold an undo handle, and almost all of them do one
thing with it: edit the library and hand the change over. Undoing,
redoing, asking whether the library differs from the last saved
position and subscribing to stack changes are the business of five
classes. Passing the whole manager to the rest handed every field
editor, cleanup and import task the ability to rewrite the user's
history, when all any of them does is describe what it just changed.

`UndoManager` is now the recording interface — `addEdit(BibChange)`,
`addEdit(String, Consumer)`, `applyEdit(BibChange)` — and
`JabRefUndoManager` is the implementation, following the shape JabRef
already uses for DialogService / JabRefDialogService. The class header
argued against a separate recording type on the grounds that it "would
mean threading a second handle everywhere the first one already goes";
that holds against a second object and not against an interface on the
same one, and the header now says so.

Keeping the interface under the old name is what makes this small. The
~118 recording clients keep the type name and the handle name they
already had, so the diff is 21 files rather than 133, and afterburner
keeps resolving injection sites because the registered key still
matches the declared type. `JabRefGUI` registers the one instance under
both keys, for the few classes that ask for the implementation.

Only five classes call anything beyond recording: GuiUndoManager,
UndoAction, RedoAction, EditAction and LibraryTab (SaveDatabaseAction
reaches markUnchanged through LibraryTab's getter). Sixteen further
classes hold the implementation and call nothing on it at all — they
carry it to reach one of those five, which is P12's problem, not an
interface gap: what they would need is undo/redo/canUndo/hasChanged,
which is the whole class again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
P16 left the implementation type threaded through the GUI: twenty
classes named `JabRefUndoManager`, and only five of them called
anything on it. The other fifteen carried it to reach one of those
five, so narrowing them meant narrowing what they carried it *to*.

The layering JabRef already uses for preferences does that in one
move:

    CliPreferences  <- JabRefCliPreferences
    GuiPreferences extends CliPreferences
        JabRefGuiPreferences extends JabRefCliPreferences implements GuiPreferences

    UndoManager  <- JabRefUndoManager
    GuiUndoManager extends UndoManager
        JabRefGuiUndoManager extends JabRefUndoManager implements GuiUndoManager

`GuiUndoManager` is now an interface: the recording half it inherits,
the stack controls the undo UI needs, and the two JavaFX properties the
menus bind to. It declares the controls rather than inheriting them
because an interface cannot inherit from a class; `JabRefUndoManager`
already implements every one, and `JabRefGuiUndoManager` brings the two
together.

No class type is threaded anywhere now. In jabgui, `JabRefUndoManager`
appears only in that extends clause, and `JabRefGuiUndoManager` only in
`JabRefGUI`, which creates the single instance and registers it under
both interfaces — as `Launcher` registers `JabRefGuiPreferences` under
`GuiPreferences.class`.

Extending rather than wrapping also removes the unwrap: `UndoAction`
and `RedoAction` already received the facade and did
`guiUndoManager.getUndoManager().undo()`. That accessor is gone, and
with it the second object the application never wanted — `JabRefFrame`
no longer builds a facade around the manager it was passed, and
`MainMenu` and `MainToolBar` take one handle where they took two.

This is inheritance for a layer specialization, which is why it does
not contradict the P11 follow-up two commits ago: that subclass hung a
dialog's counter on a value type and the owner never read it, while
this adds the GUI's view of a service, the shape this codebase already
uses for preferences and dialogs. The cost is that `JabRefUndoManager`
is now an extension point: it stays non-final, the subclass shares its
monitor, and the refresh runs from a listener the subclass registers on
itself, after the monitor is released.

Three tests that had to mock the class can now mock an interface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
`CompoundEdit` takes any string and `toChangeSet()` hands it on as
`ChangeSet.name`, which that record's javadoc calls "the only
user-facing text in the change model". Nothing converts, validates or
localizes it in between, so the promotion from developer label to user
text happens silently at that one call.

Eight of the twenty-six step names had never been written for a reader:
CHANGE_SELECTED_FIELD, APPEND_TO_SELECTED_FIELD, CLEAR_SELECTED_FIELD,
COPY_FIELD_VALUE, MOVE_EDIT, SWAP_FIELD_VALUES, RENAME_EDIT, and
EDIT_FIELDS — the last published as `NAMED_COMPOUND_EDITS`, a leftover
of the `NamedCompoundEdit` class workstream A deleted. Each now carries
the label of the control the user actually activated: Set, Append,
Clear field content, Copy content, Move content, Swap content, Rename
field, and the dialog's own title for the step that spans all of them.
Every one of those was already a translated key, so no key is added and
none retired.

Four more names were hand-copies of a StandardActions label — three
"Merge entries" and one "Automatically set file links" — and now come
from the action, which is where the text the user just clicked already
lives and where it will stay correct when the menu wording changes.

Nothing renders these yet: the sole reader is the warning ChangeSet
logs when part of a set fails to apply. P5 is what will show them, and
this is its prerequisite — a name written for a log is a name written
for a person, or P5 ships "Could not fully apply CHANGE_SELECTED_FIELD".
The javadoc on both types now states that rule instead of leaving it to
the field's type.

`AutomaticFieldEditorViewModel#cancelChanges` is documented rather than
changed: it reverts what the tabs already wrote and records nothing,
which is correct because nothing reaches the stack until OK — but it is
the one deliberate write outside the journal and read like the defect
`applyEdit` exists to prevent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
Three step names described the command the user had just invoked, in
wording that differed from the command's own label:

    "Autogenerate citation keys"  ->  GENERATE_CITE_KEYS  "Generate citation keys"
    "Update keywords"             ->  MANAGE_KEYWORDS     "Manage keywords"
    "Replace string"              ->  REPLACE_ALL         "Find and replace"

Each now takes its text from the action, so the undo step says what the
menu item said, and stays right when the menu wording changes. That
also settles the casing drift, which was the visible symptom: "Replace
string" beside a dialog titled "Replace String" beside a menu entry
reading "Find and replace", for one operation.

A fourth, "duplicate removal", is deliberately left alone, because the
rule is not "use the action label" but "name what will be reversed".
Find duplicates only searches; the removals and merges come from the
user's decisions about each pair afterwards, so the action's label
describes something no undo can take back. A comment at the call site
says so, since the substitution looks obvious until you read the step.

"Update keywords" existed only as a step name, so its key is removed
from the English properties file — the same handling A3 gave the 19
presentation-name keys it retired, with the translations left to
Crowdin. The other keys stay: KeyBinding still uses them.

Nothing rendered these names before this commit and nothing renders
them after it, so no user-visible text changes today. What changes is
that P5 can surface any of them without first having to ask whether
this particular one was written for a person.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
Undo and Redo in `EditorContextAction` act on the text control's own
stack. That is correct for the search field, whose text is not a value
of the library, and wrong for a field editor, where `Ctrl+Z` is routed
to the library's journal by the filter in `FieldEditorFX` (#11420): a
menu Undo would revert the control's text, the text listener would see
that as a fresh edit, and the undo the user asked for would land on the
stack as a new forward change.

`getDefaultContextMenuItems` has left both items out since 2019
(d1307a8), so the hazard is already avoided — but the avoidance
rested on a parenthesis reading "(except undo/redo)", with nothing to
tell the next contributor why the omission is deliberate or what to do
instead. Both javadocs now say it.

No behaviour change; the two items are still built for the search
field, which is the one control they suit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
Undo covers the bibliography, not the file system (decision D7). Two
operations left the user to discover that for themselves.

Deleting a linked file removes it from disk and journals the entry's
`file` field, so undo brings the link back and points it at a file that
is no longer there. Renaming one is worse in a quieter way: the rename
happens on disk, only the link is journalled, so undoing restores the
old link — a name no file has any more.

Both already ask before acting, so both now say so at the point where
the user decides, rather than notifying afterwards about something
already done.

Not covered, deliberately: renaming to the suggested name, moving to a
directory, downloading, and the file cleanups have no decision point to
attach a sentence to, and a toast on every such operation is noise that
teaches people to dismiss toasts. The cleanup that renames PDFs already
asks its own "does not support undo" question, which is the pattern to
follow if the others ever grow one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
One sentence each, under twenty words, starting with "We fixed" and
"We changed", with no internals named. The links were pointing at
#16627, the already-merged predecessor of this branch; they are TODO
placeholders until this pull request has a number, per AGENTS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
`Deque.peek()` answers "empty" with `null`, so reading the top of a
stack meant a null check in three places. `isEmpty()` plus
`getFirst()` says the same thing without one, and keeps the order that
undo needs: the inverse is applied before the entry moves across, so a
change that throws stays undoable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
The logger still named `UndoManager`, which is now the interface, so
every message from the journal was tagged with a type that has no
implementation of its own. Caught by `rewriteDryRun`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
`MainTableColumnModel` asked the injector for an undo journal in its
constructor, although only `getDisplayName()` needs one, and only for
a special field's label.

Preference migrations run in `Launcher` before `JabRefGUI` registers
anything, and they build column models. While `UndoManager` was a
class, afterburner quietly reflected a throwaway instance into
existence; now that it is an interface, the same call throws "Cannot
instantiate view" and takes the startup down with it — for the users
whose stored preferences still need that migration.

Moving the lookup to the point of use fixes the crash and removes a
dependency a column-preference value object never had a reason to hold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
@calixtus
calixtus marked this pull request as ready for review August 25, 2026 14:21
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Make undo history identity-safe and separate recording from GUI control

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Tracks saved history positions uniquely, preventing unsaved edits from appearing saved.
• Applies and records edits atomically while separating recording, stack, and JavaFX contracts.
• Replaces internal undo labels and clarifies filesystem operations that undo cannot reverse.
Diagram

classDiagram
class EditingClients
class UndoManager {
  <<interface>>
  +addEdit(change)
  +applyEdit(change)
}
class JabRefUndoManager {
  +undo()
  +redo()
  +hasChanged()
}
class GuiUndoManager {
  <<interface>>
  +undoableProperty()
  +redoableProperty()
}
class JabRefGuiUndoManager
class UndoUI
class ChangeSet
UndoManager <|.. JabRefUndoManager
UndoManager <|-- GuiUndoManager
JabRefUndoManager <|-- JabRefGuiUndoManager
GuiUndoManager <|.. JabRefGuiUndoManager
EditingClients --> UndoManager : records edits
UndoUI --> GuiUndoManager : drives stacks
UndoManager --> ChangeSet : groups edits
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep a composed JavaFX wrapper
  • ➕ Preserves strict separation between the plain journal and JavaFX state.
  • ➕ Avoids inheritance between the core and GUI implementations.
  • ➖ Requires two runtime objects and forwarding accessors.
  • ➖ Encourages callers to unwrap the manager, weakening capability-based typing.
2. Represent saved state with stack revisions
  • ➕ Requires less journal metadata.
  • ➕ Closely resembles the previous implementation.
  • ➖ Cannot distinguish divergent histories at equal depth or net balance.
  • ➖ Can again misclassify discarded saved positions as reachable.
3. Use immutable history snapshots
  • ➕ Makes branch identity and saved positions explicit.
  • ➕ Could support richer history visualization later.
  • ➖ Adds substantially more memory and implementation complexity.
  • ➖ Is unnecessary for the current bounded linear undo/redo model.

Recommendation: Keep the PR's monotonic position IDs and capability-oriented interfaces. They directly fix divergent-history correctness with low runtime cost, while the single inherited GUI implementation avoids wrapper leakage; composition is cleaner in isolation but was already producing two handles for one application journal.

Files changed (70) +784 / -463

Enhancement (17) +128 / -50
GenerateCitationKeyAction.javaName citation-key undo steps from the action +2/-1

Name citation-key undo steps from the action

• Uses the localized standard action text for grouped citation-key generation edits.

jabgui/src/main/java/org/jabref/gui/citationkeypattern/GenerateCitationKeyAction.java

DuplicateSearch.javaClarify duplicate-removal undo semantics +4/-2

Clarify duplicate-removal undo semantics

• Names the grouped effect for readers and migrates removal/insertion changes to applyEdit.

jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateSearch.java

ManageKeywordsViewModel.javaUse the Manage Keywords action as the undo name +2/-2

Use the Manage Keywords action as the undo name

• Aligns the grouped undo label with the user-invoked standard action.

jabgui/src/main/java/org/jabref/gui/edit/ManageKeywordsViewModel.java

ReplaceStringViewModel.javaUse the Replace All action as the undo name +2/-2

Use the Replace All action as the undo name

• Names replacement history with the localized action presented to users.

jabgui/src/main/java/org/jabref/gui/edit/ReplaceStringViewModel.java

AutomaticFieldEditorViewModel.javaGive the dialog one localized undo step +11/-2

Give the dialog one localized undo step

• Names the dialog-wide compound edit for users and documents why cancellation reverts without journal recording.

jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/AutomaticFieldEditorViewModel.java

ClearContentViewModel.javaLocalize clear-content undo steps +3/-4

Localize clear-content undo steps

• Uses a standard CompoundEdit with a user-facing name and passes the affected count separately.

jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/clearcontent/ClearContentViewModel.java

CopyOrMoveFieldContentTabViewModel.javaLocalize copy, move, and swap undo steps +10/-13

Localize copy, move, and swap undo steps

• Replaces developer tokens with localized labels, uses applyEdit, and separates notification counts from history values.

jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/copyormovecontent/CopyOrMoveFieldContentTabViewModel.java

EditFieldContentViewModel.javaLocalize set and append undo steps +5/-7

Localize set and append undo steps

• Replaces internal tokens with user-facing operation names and passes affected counts independently.

jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/editfieldcontent/EditFieldContentViewModel.java

RenameFieldViewModel.javaLocalize rename-field undo steps +3/-5

Localize rename-field undo steps

• Uses a standard localized CompoundEdit and reports the affected count outside the change object.

jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/renamefield/RenameFieldViewModel.java

CitationRelationsTab.javaAlign citation merge history with the merge action +4/-3

Align citation merge history with the merge action

• Uses the standard merge label and applyEdit for grouped database replacement.

jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java

AutoLinkFilesAction.javaName auto-link history from its standard action +2/-1

Name auto-link history from its standard action

• Uses the localized Auto Link Files action text for the compound edit.

jabgui/src/main/java/org/jabref/gui/externalfiles/AutoLinkFilesAction.java

LinkedFileViewModel.javaWarn that rename undo only restores the link +4/-1

Warn that rename undo only restores the link

• Extends the rename prompt to explain that the filesystem name itself is not reverted.

jabgui/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java

DeleteFileAction.javaWarn that undo cannot restore deleted files +4/-1

Warn that undo cannot restore deleted files

• Adds localized guidance to the deletion decision dialog about the journal's filesystem boundary.

jabgui/src/main/java/org/jabref/gui/linkedfile/DeleteFileAction.java

BatchEntryMergeTask.javaName batch merges from the standard action +2/-1

Name batch merges from the standard action

• Uses the localized Merge Entries action text for grouped batch updates.

jabgui/src/main/java/org/jabref/gui/mergeentries/BatchEntryMergeTask.java

MergeTwoEntriesAction.javaAlign two-entry merge history with the action +4/-4

Align two-entry merge history with the action

• Uses the standard merge label and applyEdit for grouped insertion and removal.

jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/MergeTwoEntriesAction.java

JabRefGuiUndoManager.javaAdd the unified JavaFX-aware undo implementation +64/-0

Add the unified JavaFX-aware undo implementation

• Extends the plain journal, implements GuiUndoManager, and refreshes stack properties safely on the JavaFX thread.

jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java

JabRef_en.propertiesLocalize filesystem undo limitations +2/-1

Localize filesystem undo limitations

• Adds English messages for delete and rename warnings and removes the superseded keyword undo label.

jablib/src/main/resources/l10n/JabRef_en.properties

Bug fix (6) +344 / -13
LibraryTab.javaDepend on the GUI undo capability +7/-7

Depend on the GUI undo capability

• Uses GuiUndoManager where the tab must inspect and drive history, and adopts the atomic applyEdit API.

jabgui/src/main/java/org/jabref/gui/LibraryTab.java

AbstractEditorViewModel.javaAtomically apply field-editor changes +1/-1

Atomically apply field-editor changes

• Routes field updates through UndoManager.applyEdit so model mutation and journal recording share one operation.

jabgui/src/main/java/org/jabref/gui/fieldeditors/AbstractEditorViewModel.java

PreamblePropertiesViewModel.javaAtomically apply preamble edits +1/-1

Atomically apply preamble edits

• Uses UndoManager.applyEdit for preamble changes.

jabgui/src/main/java/org/jabref/gui/libraryproperties/preamble/PreamblePropertiesViewModel.java

MainTableColumnModel.javaResolve undo lazily for special-field labels +5/-3

Resolve undo lazily for special-field labels

• Defers UndoManager lookup until display-name generation so preference migration can run before GUI service registration.

jabgui/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java

ContentSelectorColumn.javaMigrate selector edits to applyEdit +1/-1

Migrate selector edits to applyEdit

• Uses the atomic recording API when an undo manager is available.

jabgui/src/main/java/org/jabref/gui/maintable/columns/ContentSelectorColumn.java

JabRefUndoManager.javaImplement an identity-safe, synchronized undo journal +329/-0

Implement an identity-safe, synchronized undo journal

• Moves stack behavior into a concrete plain-Java manager, assigns monotonic position IDs, atomically applies and records changes, preserves grouping, and notifies listeners outside its monitor.

jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java

Refactor (33) +139 / -356
JabRefGUI.javaRegister one GUI undo implementation under both contracts +7/-2

Register one GUI undo implementation under both contracts

• Creates a JabRefGuiUndoManager and registers the same instance as both the recording and GUI undo services.

jabgui/src/main/java/org/jabref/gui/JabRefGUI.java

EntryAdd.javaApply collaborative entry additions through the renamed API +1/-1

Apply collaborative entry additions through the renamed API

• Migrates the grouped insertion to CompoundEdit.applyEdit.

jabgui/src/main/java/org/jabref/gui/collab/entryadd/EntryAdd.java

EntryChange.javaApply collaborative replacements through the renamed API +2/-2

Apply collaborative replacements through the renamed API

• Migrates grouped removal and insertion operations to applyEdit.

jabgui/src/main/java/org/jabref/gui/collab/entrychange/EntryChange.java

EntryDelete.javaApply collaborative deletions through the renamed API +1/-1

Apply collaborative deletions through the renamed API

• Migrates the grouped removal operation to applyEdit.

jabgui/src/main/java/org/jabref/gui/collab/entrydelete/EntryDelete.java

PreambleChange.javaApply collaborative preamble changes through the renamed API +1/-1

Apply collaborative preamble changes through the renamed API

• Uses CompoundEdit.applyEdit for preamble synchronization changes.

jabgui/src/main/java/org/jabref/gui/collab/preamblechange/PreambleChange.java

BibTexStringAdd.javaApply collaborative string additions through the renamed API +1/-1

Apply collaborative string additions through the renamed API

• Uses applyEdit while retaining key-collision handling.

jabgui/src/main/java/org/jabref/gui/collab/stringadd/BibTexStringAdd.java

BibTexStringChange.javaApply collaborative string changes through the renamed API +1/-1

Apply collaborative string changes through the renamed API

• Migrates grouped string content updates to applyEdit.

jabgui/src/main/java/org/jabref/gui/collab/stringchange/BibTexStringChange.java

BibTexStringDelete.javaApply collaborative string deletions through the renamed API +1/-1

Apply collaborative string deletions through the renamed API

• Uses applyEdit while preserving removal failure logging.

jabgui/src/main/java/org/jabref/gui/collab/stringdelete/BibTexStringDelete.java

BibTexStringRename.javaApply collaborative string renames through the renamed API +1/-1

Apply collaborative string renames through the renamed API

• Migrates grouped string-name changes to applyEdit.

jabgui/src/main/java/org/jabref/gui/collab/stringrename/BibTexStringRename.java

EditAction.javaRequire GUI undo control for edit commands +3/-3

Require GUI undo control for edit commands

• Changes the action dependency from the recording-only interface to GuiUndoManager because cut handling drives history.

jabgui/src/main/java/org/jabref/gui/edit/EditAction.java

AbstractAutomaticFieldEditorTabViewModel.javaSeparate affected counts from automatic-editor changes +8/-2

Separate affected counts from automatic-editor changes

• Accepts a standard CompoundEdit plus an explicit affected-entry count, keeping notification data outside the undo value.

jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/AbstractAutomaticFieldEditorTabViewModel.java

MoveFieldValueAction.javaMigrate field moves to applyEdit +2/-2

Migrate field moves to applyEdit

• Uses the renamed compound-edit operation for destination and source field changes.

jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/MoveFieldValueAction.java

SourceTab.javaMigrate source edits to applyEdit +3/-3

Migrate source edits to applyEdit

• Uses the renamed grouped-application API for field and entry-type changes.

jabgui/src/main/java/org/jabref/gui/entryeditor/SourceTab.java

JabRefFrame.javaUse one GUI undo manager throughout the frame +4/-9

Use one GUI undo manager throughout the frame

• Removes the separate wrapper and passes the unified GUI-capable manager to actions, menus, toolbars, and editors.

jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java

MainMenu.javaDrive menu undo from the unified manager +5/-9

Drive menu undo from the unified manager

• Removes dual manager dependencies and binds Undo and Redo actions to GuiUndoManager.

jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java

MainToolBar.javaDrive toolbar undo from the unified manager +4/-8

Drive toolbar undo from the unified manager

• Removes the wrapper dependency and uses one GuiUndoManager for history controls and edit actions.

jabgui/src/main/java/org/jabref/gui/frame/MainToolBar.java

OpenDatabaseAction.javaThread the GUI undo contract into opened tabs +4/-4

Thread the GUI undo contract into opened tabs

• Requires GuiUndoManager where database opening constructs library tabs that expose stack controls.

jabgui/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java

MainTable.javaUse GUI undo capabilities in the main table +2/-2

Use GUI undo capabilities in the main table

• Changes the table's dependency to GuiUndoManager for flows that can drive history.

jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java

RightClickMenu.javaRequire GUI undo control in table context menus +2/-2

Require GUI undo control in table context menus

• Updates the context-menu factory contract to the GUI-capable manager.

jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java

OpenOfficePanel.javaUse the GUI undo contract in OpenOffice integration +3/-3

Use the GUI undo contract in OpenOffice integration

• Updates panel construction to accept the unified GUI-capable manager.

jabgui/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java

SharedDatabaseLoginDialogView.javaInject the GUI undo contract into shared login +2/-2

Inject the GUI undo contract into shared login

• Requests GuiUndoManager for downstream shared-library tab construction.

jabgui/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogView.java

SharedDatabaseLoginDialogViewModel.javaCarry GUI undo control through shared login +3/-3

Carry GUI undo control through shared login

• Changes the view model dependency to GuiUndoManager.

jabgui/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java

SharedDatabaseUIManager.javaUse GUI undo control for shared-library tabs +3/-3

Use GUI undo control for shared-library tabs

• Updates shared database UI construction to pass the GUI-capable manager.

jabgui/src/main/java/org/jabref/gui/shared/SharedDatabaseUIManager.java

SidePane.javaUse the GUI undo contract in the side pane +2/-2

Use the GUI undo contract in the side pane

• Updates side-pane construction to accept GuiUndoManager.

jabgui/src/main/java/org/jabref/gui/sidepane/SidePane.java

SidePaneContentFactory.javaPropagate GUI undo control to side-pane content +3/-3

Propagate GUI undo control to side-pane content

• Stores and forwards GuiUndoManager to content requiring library-tab capabilities.

jabgui/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java

SidePaneViewModel.javaPropagate GUI undo control through the side-pane model +2/-2

Propagate GUI undo control through the side-pane model

• Changes the view model constructor contract to GuiUndoManager.

jabgui/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java

GuiUndoManager.javaTurn GUI undo into a capability interface +32/-46

Turn GUI undo into a capability interface

• Replaces the JavaFX wrapper class with an interface combining recording, stack control, saved-state operations, listeners, and observable properties.

jabgui/src/main/java/org/jabref/gui/undo/GuiUndoManager.java

RedoAction.javaUse GuiUndoManager directly for redo +2/-3

Use GuiUndoManager directly for redo

• Removes wrapper unwrapping and drives redo through the injected capability.

jabgui/src/main/java/org/jabref/gui/undo/RedoAction.java

UndoAction.javaUse GuiUndoManager directly for undo +2/-3

Use GuiUndoManager directly for undo

• Removes wrapper unwrapping and drives undo through the injected capability.

jabgui/src/main/java/org/jabref/gui/undo/UndoAction.java

OpenLibrarySideEffect.javaResolve GUI undo control for walkthrough libraries +2/-2

Resolve GUI undo control for walkthrough libraries

• Requests GuiUndoManager when constructing a walkthrough library tab.

jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/sideeffect/OpenLibrarySideEffect.java

WelcomeTab.javaUse GUI undo control when opening welcome-tab libraries +3/-3

Use GUI undo control when opening welcome-tab libraries

• Changes the welcome tab's manager dependency to GuiUndoManager.

jabgui/src/main/java/org/jabref/gui/welcome/WelcomeTab.java

UndoManager.javaNarrow UndoManager to the recording contract +16/-223

Narrow UndoManager to the recording contract

• Replaces the concrete all-capabilities manager with an interface exposing only addEdit and applyEdit operations used by editing clients.

jablib/src/main/java/org/jabref/logic/undo/UndoManager.java

CompoundEdit.javaFinalize compound edits and rename applyEdit +11/-3

Finalize compound edits and rename applyEdit

• Prevents notification-specific subclassing, documents user-facing names, and renames grouped application to match the manager API.

jablib/src/main/java/org/jabref/model/undo/CompoundEdit.java

Tests (11) +148 / -41
ManageKeywordsViewModelTest.javaInstantiate the concrete journal in keyword tests +2/-2

Instantiate the concrete journal in keyword tests

• Updates tests after UndoManager became an interface.

jabgui/src/test/java/org/jabref/gui/edit/ManageKeywordsViewModelTest.java

ReplaceStringViewModelTest.javaUse the GUI journal in replacement tests +2/-2

Use the GUI journal in replacement tests

• Supplies JabRefGuiUndoManager to match LibraryTab's strengthened contract.

jabgui/src/test/java/org/jabref/gui/edit/ReplaceStringViewModelTest.java

SourceTabTest.javaInstantiate the concrete journal in source-tab tests +2/-2

Instantiate the concrete journal in source-tab tests

• Replaces direct interface construction with JabRefUndoManager.

jabgui/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java

SaveDatabaseActionTest.javaMock the GUI undo contract in save tests +2/-2

Mock the GUI undo contract in save tests

• Matches LibraryTab.getUndoManager's new return type.

jabgui/src/test/java/org/jabref/gui/exporter/SaveDatabaseActionTest.java

JournalEditorViewModelTest.javaInstantiate the concrete journal in journal-editor tests +2/-2

Instantiate the concrete journal in journal-editor tests

• Updates the fixture after the manager interface split.

jabgui/src/test/java/org/jabref/gui/fieldeditors/JournalEditorViewModelTest.java

LanguageEditorViewModelTest.javaInstantiate the concrete journal in language-editor tests +2/-2

Instantiate the concrete journal in language-editor tests

• Uses JabRefUndoManager as the recording implementation.

jabgui/src/test/java/org/jabref/gui/fieldeditors/optioneditors/LanguageEditorViewModelTest.java

OpenDatabaseActionTest.javaMock GUI undo control in database-open tests +2/-2

Mock GUI undo control in database-open tests

• Updates constructor expectations to GuiUndoManager.

jabgui/src/test/java/org/jabref/gui/importer/actions/OpenDatabaseActionTest.java

UpdateOriginalEntryTest.javaInstantiate the concrete journal in merge tests +3/-3

Instantiate the concrete journal in merge tests

• Uses JabRefUndoManager for undo verification.

jabgui/src/test/java/org/jabref/gui/mergeentries/UpdateOriginalEntryTest.java

SidePaneViewModelTest.javaMock GUI undo control in side-pane tests +2/-2

Mock GUI undo control in side-pane tests

• Updates the fixture to the side pane's new capability contract.

jabgui/src/test/java/org/jabref/gui/sidepane/SidePaneViewModelTest.java

JabRefGuiUndoManagerTest.javaTest the unified JavaFX undo implementation +13/-16

Test the unified JavaFX undo implementation

• Renames and adapts property tests to exercise JabRefGuiUndoManager directly, including queued and immediate refresh behavior.

jabgui/src/test/java/org/jabref/gui/undo/JabRefGuiUndoManagerTest.java

JabRefUndoManagerTest.javaCover divergent history and atomic recording +116/-6

Cover divergent history and atomic recording

• Adds regressions for identity-based saved positions, lock-held application, and grouped applyEdit behavior while migrating existing tests to the concrete manager.

jabgui/src/test/java/org/jabref/gui/undo/JabRefUndoManagerTest.java

Documentation (3) +25 / -3
CHANGELOG.mdDocument saved-state and filesystem undo behavior +2/-0

Document saved-state and filesystem undo behavior

• Adds user-facing notes for the close-without-save fix and the clarified delete/rename file dialogs.

CHANGELOG.md

EditorContextAction.javaDocument text-control versus library undo routing +15/-1

Document text-control versus library undo routing

• Explains why field-editor context menus omit control-local Undo and Redo while the search field may use them.

jabgui/src/main/java/org/jabref/gui/fieldeditors/contextmenu/EditorContextAction.java

ChangeSet.javaDefine undo names as user-recognizable action text +8/-2

Define undo names as user-recognizable action text

• Documents why grouped change names must be localized, presentation-ready descriptions rather than developer tokens.

jablib/src/main/java/org/jabref/model/undo/ChangeSet.java

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (2) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. refresh() updates are uncoalesced 📘 Rule violation ➹ Performance
Description
Every off-thread journal notification queues a separate JavaFX callback, and the new implementation
explicitly leaves bursts uncoalesced. If edits arrive faster than the FX queue drains, redundant
callbacks can accumulate without bound and delay UI work.
Code

jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java[R55-57]

+    /// A burst of edits therefore queues one update per edit, and they are not coalesced: each
+    /// reads the current state, so every update after the first sets the value already there,
+    /// which a JavaFX property ignores without notifying anything.
Evidence
Rule 38 requires repeated Platform.runLater work to be coalesced. The new class states that a
burst queues one update per edit, while UiTaskExecutor.runNowOrInJavaFXThread sends every non-FX
invocation to Platform.runLater.

jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java[55-62]
jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java[97-106]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`JabRefGuiUndoManager.refresh()` schedules one JavaFX callback for every off-thread stack notification instead of coalescing repeated updates.
## Issue Context
`UiTaskExecutor.runNowOrInJavaFXThread` delegates every off-thread call to `Platform.runLater`. Maintain a single pending refresh, clear its pending state after execution, and ensure the callback reads the latest stack state.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java[46-62]
- jabgui/src/test/java/org/jabref/gui/undo/JabRefGuiUndoManagerTest.java[91-114]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Undo fix lacks requirement ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new history-position identity and atomic apply-and-record behavior constitute a significant,
user-data-preserving bug fix, but the PR adds no OpenFastTrace requirement. This leaves the fixed
save-prompt and concurrency guarantees without required requirements traceability.
Code

jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java[R293-295]

+    public synchronized boolean hasChanged() {
+        return currentPosition() != savedId;
+    }
Evidence
Rule 29 requires a requirement for a significant bug fix. The implementation introduces unique
history-position identities specifically to prevent unsaved work from being reported as saved, while
no docs/requirements file is changed and existing requirements do not define this guarantee.

AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements: AGENTS.md: New Features and Significant Bug Fixes Must Define Requirements
jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java[83-95]
jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java[281-300]
jabgui/src/test/java/org/jabref/gui/undo/JabRefUndoManagerTest.java[269-312]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Add an OpenFastTrace-compatible requirement for the significant undo correctness fix.
## Issue Context
Document that discarded history cannot match the saved position and that applying and recording an undoable change is atomic. Follow the repository's requirement identifier placement and file-ending conventions, and link implementation/tests where required.
## Fix Focus Areas
- docs/requirements/index.md[1-63]
- jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java[124-156]
- jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java[281-300]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Undo redesign lacks MADR 📘 Rule violation ⚙ Maintainability
Description
The PR replaces one concrete manager with recording, implementation, and JavaFX-layer
interfaces/classes and explicitly chooses inheritance over wrapping, but adds no MADR or
decision-index entry. The rationale and alternatives for this cross-module architectural change
therefore remain undocumented in the architecture decision system.
Code

jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java[R19-22]

+/// Extends rather than wraps, following `JabRefGuiPreferences extends JabRefCliPreferences`.
+/// Wrapping meant every caller reached through a `getUndoManager()` accessor to do anything, and
+/// meant two objects where the application only ever has one.
+@NullMarked
Evidence
Rule 30 requires a complete and indexed MADR for a significant architecture decision. The changed
types establish new logic/GUI boundaries and explicitly document the choice to extend rather than
wrap, while the PR contains no docs/decisions change and no existing undo ADR covers the decision.

AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR: AGENTS.md: Document Significant Architecture Decisions with MADR
jablib/src/main/java/org/jabref/logic/undo/UndoManager.java[10-33]
jabgui/src/main/java/org/jabref/gui/undo/GuiUndoManager.java[9-21]
jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java[11-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Create and index a MADR for the undo-manager architecture redesign.
## Issue Context
Capture the recording-only interface, plain-Java implementation, JavaFX-specific interface/implementation, inheritance-versus-wrapping choice, threading boundary, considered alternatives, and decision outcome using the repository template.
## Fix Focus Areas
- docs/decisions/adr-template.md[1-200]
- docs/decisions/index.md[1-100]
- jablib/src/main/java/org/jabref/logic/undo/UndoManager.java[10-33]
- jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java[11-23]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (1)
4. Test uses uninitialized JavaFX ✓ Resolved 🐞 Bug ☼ Reliability
Description
ReplaceStringViewModelTest now uses JabRefGuiUndoManager without a JavaFX test extension, so any
replacement that records an edit invokes Platform.runLater and fails with “Toolkit not
initialized.” This breaks the parameterized replacement cases and the undo test in normal headless
test execution.
Code

jabgui/src/test/java/org/jabref/gui/edit/ReplaceStringViewModelTest.java[30]

+    private final JabRefGuiUndoManager undoManager = new JabRefGuiUndoManager();
Evidence
The changed test constructs the GUI-aware manager without ApplicationExtension. That manager
registers refresh on every stack change, and refresh calls
UiTaskExecutor.runNowOrInJavaFXThread, whose non-FX path calls Platform.runLater; the dedicated
manager test explicitly states that this requires a live toolkit and therefore uses
ApplicationExtension.

jabgui/src/test/java/org/jabref/gui/edit/ReplaceStringViewModelTest.java[27-30]
jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java[28-33]
jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java[58-62]
jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java[97-105]
jabgui/src/test/java/org/jabref/gui/undo/JabRefGuiUndoManagerTest.java[26-32]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ReplaceStringViewModelTest` constructs `JabRefGuiUndoManager`, whose edit listener schedules property refreshes through JavaFX. The test does not initialize JavaFX, so edit-recording cases fail with `Toolkit not initialized`.
## Issue Context
`JabRefGuiUndoManagerTest` documents this requirement and uses `ApplicationExtension`. Apply the same setup here, or replace the concrete GUI manager with a test implementation of `GuiUndoManager` that does not access JavaFX while retaining functional undo behavior.
## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/edit/ReplaceStringViewModelTest.java[27-30]
- jabgui/src/test/java/org/jabref/gui/undo/JabRefGuiUndoManagerTest.java[26-32]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Javadoc names removed apply ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new test Javadoc links to JabRefUndoManager#apply, but this PR renames that API to
applyEdit, so the reference is stale and cannot resolve. This undermines the naming accuracy
required for changed Java documentation.
Code

jabgui/src/test/java/org/jabref/gui/undo/JabRefUndoManagerTest.java[R450-453]

+    /// An entry that runs `probe` from inside `setField`, when the field is set to `value`. The
+    /// write happens on the thread making the change, so the probe runs at the one moment
+    /// [JabRefUndoManager#apply] has written to the library and not yet recorded anything — the window
+    /// this test is about.
Evidence
Rule 5 requires exact, meaningful naming in changed Java code. The added Javadoc names
JabRefUndoManager#apply, while the manager's new public method is applyEdit and repository
search finds no apply method on that class.

AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions: AGENTS.md: Follow JabRef Java Formatting and Naming Conventions
jabgui/src/test/java/org/jabref/gui/undo/JabRefUndoManagerTest.java[450-453]
jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java[141-155]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Correct the stale Javadoc method reference from `apply` to `applyEdit`.
## Issue Context
The PR removes the old `apply` API and introduces `JabRefUndoManager.applyEdit`; the new test documentation should use the exact current method name.
## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/undo/JabRefUndoManagerTest.java[450-453]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java
Comment thread jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java
Comment thread jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java
Comment thread jabgui/src/test/java/org/jabref/gui/undo/JabRefUndoManagerTest.java
Comment thread jabgui/src/test/java/org/jabref/gui/edit/ReplaceStringViewModelTest.java Outdated
calixtus and others added 6 commits August 25, 2026 16:54
The checklist asks for a `docs/requirements/` entry when a change is a
significant bug fix, and both halves of this branch qualify: one
decides whether a library can be closed while holding unsaved work, the
other whether the recorded history can describe a state the library
never had.

`docs/requirements/undo.md` states them as behaviour rather than
mechanism — a library is unmodified exactly when its history stands at
the position it was saved at, and a change performed through the
journal reaches the library and the stack as one operation. The journal
and the tests that pin them carry the OpenFastTrace links, so
`traceRequirements` reports both covered; removing either link makes it
report the requirement uncovered, which is how the coverage was
verified rather than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
`ReplaceStringViewModelTest` needs something typed as `GuiUndoManager`,
because that is what `LibraryTab#getUndoManager` returns, and it needs a
real journal, because it asserts what undo does. It was given
`JabRefGuiUndoManager`, which refreshes its properties through the
JavaFX thread on every push.

The test passes either way — the journal catches what a listener throws
— so what this actually produced was thirteen swallowed
`IllegalStateException: Toolkit not initialized`, with stack traces, in
the log of a test that has nothing to do with JavaFX. It also left the
test resting on that catch: the day a listener failure stops being
swallowed, an unrelated test would start failing.

`HeadlessGuiUndoManager` is the journal with properties that follow the
stacks directly, without the hop — a JavaFX property needs no toolkit,
only `Platform.runLater` does. So a test reading them sees the truth
rather than a constant `false`, and the test now asserts that. What the
double skips is the marshalling, which is what makes it unfit for
anything but a single-threaded test, and which
`JabRefGuiUndoManagerTest` covers by starting the toolkit on purpose.

Regression from this branch: before the GUI layering, the test used a
plain journal, which had no JavaFX in it at all.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes undo-history correctness and separates recording, core journal, and JavaFX-facing APIs.

Changes:

  • Identifies saved history positions instead of counting edits.
  • Adds atomic apply-and-record operations and layered undo interfaces.
  • Improves undo labels and file-operation warnings.

Reviewed changes

Copilot reviewed 74 out of 74 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
CHANGELOG.md Documents user-visible fixes.
docs/decisions/0070-split-the-undo-manager-into-recording-and-gui-layers.md Records the undo architecture.
docs/requirements/undo.md Defines undo correctness requirements.
jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java Implements the core journal.
jablib/src/main/java/org/jabref/logic/undo/UndoManager.java Defines the recording interface.
jablib/src/main/java/org/jabref/model/undo/ChangeSet.java Clarifies change-set naming.
jablib/src/main/java/org/jabref/model/undo/CompoundEdit.java Renames and documents edit application.
jablib/src/main/resources/l10n/JabRef_en.properties Adds undo warnings.
jabgui/src/main/java/org/jabref/gui/JabRefGUI.java Registers the layered manager.
jabgui/src/main/java/org/jabref/gui/LibraryTab.java Uses the GUI manager API.
jabgui/src/main/java/org/jabref/gui/citationkeypattern/GenerateCitationKeyAction.java Uses action-based undo naming.
jabgui/src/main/java/org/jabref/gui/collab/entryadd/EntryAdd.java Adopts applyEdit.
jabgui/src/main/java/org/jabref/gui/collab/entrychange/EntryChange.java Adopts applyEdit.
jabgui/src/main/java/org/jabref/gui/collab/entrydelete/EntryDelete.java Adopts applyEdit.
jabgui/src/main/java/org/jabref/gui/collab/preamblechange/PreambleChange.java Adopts applyEdit.
jabgui/src/main/java/org/jabref/gui/collab/stringadd/BibTexStringAdd.java Adopts applyEdit.
jabgui/src/main/java/org/jabref/gui/collab/stringchange/BibTexStringChange.java Adopts applyEdit.
jabgui/src/main/java/org/jabref/gui/collab/stringdelete/BibTexStringDelete.java Adopts applyEdit.
jabgui/src/main/java/org/jabref/gui/collab/stringrename/BibTexStringRename.java Adopts applyEdit.
jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateSearch.java Updates grouped edit application.
jabgui/src/main/java/org/jabref/gui/edit/EditAction.java Accepts the GUI manager API.
jabgui/src/main/java/org/jabref/gui/edit/ManageKeywordsViewModel.java Uses user-facing undo names.
jabgui/src/main/java/org/jabref/gui/edit/ReplaceStringViewModel.java Uses the invoked action name.
jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/AbstractAutomaticFieldEditorTabViewModel.java Separates counts from edits.
jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/AutomaticFieldEditorUndoableEdit.java Removes the obsolete subclass.
jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/AutomaticFieldEditorViewModel.java Groups dialog edits.
jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/MoveFieldValueAction.java Adopts applyEdit.
jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/clearcontent/ClearContentViewModel.java Uses localized edit naming.
jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/copyormovecontent/CopyOrMoveFieldContentTabViewModel.java Updates copy, move, and swap edits.
jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/editfieldcontent/EditFieldContentViewModel.java Updates set and append edits.
jabgui/src/main/java/org/jabref/gui/edit/automaticfieldeditor/renamefield/RenameFieldViewModel.java Updates rename edits.
jabgui/src/main/java/org/jabref/gui/entryeditor/SourceTab.java Adopts applyEdit.
jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java Updates merge recording.
jabgui/src/main/java/org/jabref/gui/externalfiles/AutoLinkFilesAction.java Uses action-based undo naming.
jabgui/src/main/java/org/jabref/gui/fieldeditors/AbstractEditorViewModel.java Applies field edits atomically.
jabgui/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java Warns about rename undo limits.
jabgui/src/main/java/org/jabref/gui/fieldeditors/contextmenu/EditorContextAction.java Documents text-control undo behavior.
jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java Uses one GUI undo manager.
jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java Consolidates undo dependencies.
jabgui/src/main/java/org/jabref/gui/frame/MainToolBar.java Consolidates undo dependencies.
jabgui/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java Accepts the GUI manager.
jabgui/src/main/java/org/jabref/gui/libraryproperties/preamble/PreamblePropertiesViewModel.java Applies preamble edits atomically.
jabgui/src/main/java/org/jabref/gui/linkedfile/DeleteFileAction.java Warns about disk deletion.
jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java Uses the GUI manager type.
jabgui/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java Defers undo-manager lookup.
jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java Accepts the GUI manager.
jabgui/src/main/java/org/jabref/gui/maintable/columns/ContentSelectorColumn.java Adopts applyEdit.
jabgui/src/main/java/org/jabref/gui/mergeentries/BatchEntryMergeTask.java Uses action-based naming.
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/MergeTwoEntriesAction.java Updates merge recording.
jabgui/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java Uses the GUI manager type.
jabgui/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogView.java Injects the GUI manager.
jabgui/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java Uses the GUI manager type.
jabgui/src/main/java/org/jabref/gui/shared/SharedDatabaseUIManager.java Uses the GUI manager type.
jabgui/src/main/java/org/jabref/gui/sidepane/SidePane.java Uses the GUI manager type.
jabgui/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java Propagates the GUI manager.
jabgui/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java Accepts the GUI manager.
jabgui/src/main/java/org/jabref/gui/undo/GuiUndoManager.java Defines the JavaFX-facing interface.
jabgui/src/main/java/org/jabref/gui/undo/JabRefGuiUndoManager.java Implements JavaFX properties.
jabgui/src/main/java/org/jabref/gui/undo/RedoAction.java Uses the unified GUI manager.
jabgui/src/main/java/org/jabref/gui/undo/UndoAction.java Uses the unified GUI manager.
jabgui/src/main/java/org/jabref/gui/walkthrough/declarative/sideeffect/OpenLibrarySideEffect.java Resolves the GUI manager.
jabgui/src/main/java/org/jabref/gui/welcome/WelcomeTab.java Propagates the GUI manager.
jabgui/src/test/java/org/jabref/gui/edit/ManageKeywordsViewModelTest.java Updates manager construction.
jabgui/src/test/java/org/jabref/gui/edit/ReplaceStringViewModelTest.java Uses the headless GUI manager.
jabgui/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java Updates manager construction.
jabgui/src/test/java/org/jabref/gui/exporter/SaveDatabaseActionTest.java Updates manager mocking.
jabgui/src/test/java/org/jabref/gui/fieldeditors/JournalEditorViewModelTest.java Updates manager construction.
jabgui/src/test/java/org/jabref/gui/fieldeditors/optioneditors/LanguageEditorViewModelTest.java Updates manager construction.
jabgui/src/test/java/org/jabref/gui/importer/actions/OpenDatabaseActionTest.java Updates manager mocking.
jabgui/src/test/java/org/jabref/gui/mergeentries/UpdateOriginalEntryTest.java Updates manager construction.
jabgui/src/test/java/org/jabref/gui/sidepane/SidePaneViewModelTest.java Updates manager mocking.
jabgui/src/test/java/org/jabref/gui/undo/HeadlessGuiUndoManager.java Adds a headless test implementation.
jabgui/src/test/java/org/jabref/gui/undo/JabRefGuiUndoManagerTest.java Tests JavaFX property synchronization.
jabgui/src/test/java/org/jabref/gui/undo/JabRefUndoManagerTest.java Tests journal correctness and locking.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java Outdated
Comment on lines +144 to +146
CompoundEdit compound = active.get();
if (compound != null) {
compound.applyEdit(change);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inherited from before. Noted for follow up pr. too broad to fix in this pr.

calixtus and others added 4 commits August 25, 2026 19:58
…'s price

Four points from an automated review of this branch. Three describe
behaviour that predates it; each is answered where this branch can
answer it.

A block that dies on an `Error` — a model assertion, a stack overflow
on a deep nested set — skipped the handover entirely, because it sat
after a `catch (RuntimeException)`. Everything the block had already
written to the library became un-undoable, which is precisely what the
catch existed to prevent. The handover moves into the `finally`, so
whatever ends the block hands over what it collected and the failure
travels on untouched. The `failure` variable disappears with it. A test
covers the `Error` path.

The class javadoc said applying happens inside the lock and that
foreign code runs outside it, which cannot both hold for `applyEdit`.
The rule it actually imposes is now written down: a change's `apply()`
must write to the model and return, never wait for another thread, or
it deadlocks against a menu refresh already blocked in `canUndo()`.
Every change today is a plain model write; `undo()` has applied under
the lock since the first PR of this series.

`clear()` has no caller anywhere, and cannot get one while a single
journal serves every library — closing one would discard the others'
history. Its javadoc says that instead of implying a caller exists.

The requirement for the saved position claimed a per-library rule that
one shared journal cannot honour: with several libraries open, the
saved position of one is the saved position of all. It now records that
limit rather than overstating the guarantee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
A trimmed change stays applied to the library, so an empty undo stack
does not mean "the state the library started in" — it means "that
change, and nothing after it". Handing the empty stack a fresh id
treated it as a position nothing could match, which is wrong at exactly
one boundary: save a change, push past the limit until that change is
trimmed, undo everything still on the stack, and the library is the one
that was saved, while `hasChanged()` insisted it was modified for the
rest of the session.

The empty stack now inherits the id of the change that fell off, and
repeated trimming walks that identity forward one dropped change at a
time. `clear()` keeps taking a fresh id, because it discards history the
library keeps — the two cases look alike and are opposites.

The reasoning this corrects was mine, in the first PR of the pair: I
read "the saved entry is gone from the stack" as "the saved state is
unreachable". The stack entry is gone; the state it denoted is where
undoing everything lands.

The new test sits between the two existing trim tests, which pin the
cases either side of it and still pass unchanged. The requirement text
claiming every trimmed saved position is unreachable is corrected too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
The javadoc on `applyEdit` said that inside a recording block "there is
no window to close", which is not true and was mine. A block applies
each change to the library as it goes and pushes the step only when it
ends, so the window is open for the whole block. What the thread-local
recorder rules out is two threads writing one recorder — not an undo
interleaving with a block's writes.

The javadoc now says that, and says why this class cannot close it
alone: only the command knows when its block ends. P21 in the plan
records the interleaving with the field-level walkthrough, the two
candidate policies — undo waits, or undo refuses while a command is
writing — and the constraint that either must also cover the five
manual collectors that mutate on background tasks, or it will look
complete while the noisiest paths stay unguarded.

No behaviour change: this is the same shape every version of the code
has had, the Swing one included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
"...without asking to save unsaved changes" is broader than what
shipped: with several libraries open, one journal still holds one saved
position for all of them, so a library can still be closed while
holding work another library's save marked as saved. The entry now
names the case that is fixed — changes made after an undo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194MmVeMweN5EKQEx5yNoC1
@calixtus calixtus changed the title Fix undo B - Fixes to internal flaws Fix undo B - Ids on undo changes, rewordings and split of UndoManager, minor clarifications Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants