Verified Owner
Top Description Inners Fields Constructors Methods
org.jabref.logic.undo

public Class JabRefUndoManager

extends Object
implements UndoManager
Class Inheritance
All Implemented Interfaces
org.jabref.logic.undo.UndoManager
Annotations
@NullMarked
Imports
java.util.ArrayDeque, .Deque, .LinkedHashSet, .List, .Objects, .Optional, .Set, java.util.concurrent.CopyOnWriteArrayList, java.util.function.Consumer, .Supplier, org.jabref.model.undo.ApplyResult, .BibChange, .ChangeSet, .CompoundEdit, .UndoableFieldChange, org.jspecify.annotations.NullMarked, .Nullable, org.slf4j.Logger, .LoggerFactory

The undo journal: a stack of changes, and the API for putting changes on it.

Recording and undoing live on the same object on purpose: a command already holds the journal in order to record, so a separate recording object would mean threading a second handle everywhere the first one goes. A separate interface costs nothing, and UndoManager is that — the recording half, which is all the ~120 classes holding a handle need. This type is for the few that also drive the stacks.

Plain Java on purpose. Nothing here hops to the JavaFX thread, so recording works in a plain unit test and the journal could in time be used outside the GUI. Menu enablement subscribes through org.jabref.gui.undo.GuiUndoManager, which owns that hop.

Commands push from background tasks, so each operation holds this object's monitor for exactly as long as it touches the stacks:

Listeners are told that the stacks changed, never what changed, so they read the state they need when they run. Two threads recording concurrently may therefore notify in the opposite order to the one they pushed in, which does not matter: the worst case is being told twice about the same state.

Nested and Inner Type Summary

Modifier and TypeClass and Description
private class
JabRefUndoManager.Suspension

One command's claim on this library, ended by close.

private static record
JabRefUndoManager.UndoJournalEntry

A change together with the identity of the position it occupies in the history.

Field Summary

Modifier and TypeField and Description
private final ThreadLocal<@Nullable CompoundEdit>
active

Set exactly while a addEdit block is in progress on this thread.

private final ThreadLocal<Boolean>
applying

Set while this journal is applying a change on this thread, in applyEdit, undo or redo.

private final CoalescingPolicy
coalescingPolicy

The rule for when a change continues the step on top of the stack.

private long
emptyStackId

The identity of the empty undo stack: the position the library stands at once everything still on the stack has been undone.

private static final int
LIMIT

Same depth javax.swing.undo.UndoManager defaulted to.

private final List<Runnable>
listeners

Notified after every change to either stack.

private static final Logger
private static final long
NEVER_SAVED

A saved position no position will ever match, so the library counts as changed until it is saved again.

private long
nextId

Source of position ids.

private static final long
ORIGIN

The id of the empty stack before anything was ever pushed.

private final Deque<JabRefUndoManager.UndoJournalEntry>
private long
savedId

The position the library was last saved at, as an id rather than a count.

private boolean
stepFinished

Whether the step on top of the stack is finished, so that the next change starts a new one rather than continuing it.

private final Set<JabRefUndoManager.Suspension>
suspensions

The commands currently applying changes they have not yet handed over, in the order they started.

private final Deque<JabRefUndoManager.UndoJournalEntry>

Constructor Summary

AccessConstructor and Description
public
public
JabRefUndoManager(CoalescingPolicy coalescingPolicy)

For tests that want every change to be its own step, or a rule of their own.

Method Summary

Modifier and TypeMethod and Description
public void
addEdit(BibChange change)

Implements org.jabref.logic.undo.UndoManager.addEdit.

Records a single change as its own undo step, or as part of the enclosing step when called inside addEdit.

public boolean

Returns:

whether anything was recorded, for callers that report the outcome to the user
addEdit
(String name, Consumer<CompoundEdit> mutations)

Implements org.jabref.logic.undo.UndoManager.addEdit.

Runs mutations, recording whatever it reports, and pushes the result as one undo step named name.

public void
addListener(Runnable listener)

Registers a listener, from any thread and at any time — including from inside another listener, since notifyListeners iterates a snapshot.

public ApplyResult

Returns:

what was applied, and what was not — see BibChange#apply
applyEdit
(BibChange change, EditSource source)

Implements org.jabref.logic.undo.UndoManager.applyEdit.

Performs change and records it in one go, so that the write to the library and the undo step describing it cannot drift apart — neither at the call site, which cannot do one without the other, nor in time, since both happen under one acquisition of the monitor.

private ApplyResult
public synchronized boolean
public synchronized boolean
canUndo()

Whether there is a step to take back — the stack only, not whether a command is holding the library.

public void
private synchronized long
currentPosition()

The identity of the position the history is at: the change on top of the undo stack, or emptyStackId when everything has been undone.

public void
endStep()

Implements org.jabref.logic.undo.UndoManager.endStep.

Ends the step being collected, so that the next change starts one of its own.

public synchronized boolean
hasChanged()

Whether the library differs from the last saved position.

public boolean
isApplying()

Whether this journal is applying a change on this thread right now.

private static boolean
isEmptyStep(BibChange change)

Whether change would be an undo step that does nothing.

private static boolean
isNoOp(BibChange change)

Whether a merged run ended where it started.

public void
markChanged()

Implements org.jabref.logic.undo.UndoManager.markChanged.

Marks the library as changed by something this journal cannot take back — a migration on load, a shared-database update, a setting written without being recorded.

public void
markUnchanged()

Marks the current position as saved.

private void
notifyListeners()

Runs every listener even if one of them throws.

private void
push(BibChange change)

Puts change on the undo stack as a new position and discards the redo stack, which the new change has made unreachable.

private void
record(BibChange change, EditSource source)

Records change, either as a step of its own or as the continuation of the step on top of the stack — see CoalescingPolicy.

public Optional<UndoStep>
redo()

Returns: what was redone, in the shape undo returns it, or empty if there was nothing to redo or a command is holding the library

public synchronized Optional<String>
suspendedBy()

The command currently holding this library, if one is: of those still writing, the one that started first.

public UndoSuspension
suspendUndo(String name)

Implements org.jabref.logic.undo.UndoManager.suspendUndo.

Suspends undo and redo for this library until the returned suspension is closed.

public Optional<UndoStep>

Returns:

what was undone — its name for the user, and what of it could not be applied — or empty if there was nothing to undo, or a command is holding the library (see suspendUndo; suspendedBy tells the two apart). The name is taken inside the monitor, and it is all that leaves the journal, so nothing outside reads the stacks.
undo
()

Applies the inverse before moving the change across, so a change that throws stays undoable instead of vanishing from both stacks.

Inherited from java.lang.Object:
cloneequalsfinalizegetClasshashCodenotifynotifyAlltoStringwaitwaitwait

Field Detail

activeback to summary
private final ThreadLocal<@Nullable CompoundEdit> active

Set exactly while a addEdit block is in progress on this thread. Per-thread because there is one manager for the application and long commands record from background tasks: a shared field would fold edits the user makes meanwhile into the background command's step, and would have two threads appending to one recorder's list.

applyingback to summary
private final ThreadLocal<Boolean> applying

Set while this journal is applying a change on this thread, in applyEdit, undo or redo. See isApplying for what it is for and why a thread-local is sound here.

coalescingPolicyback to summary
private final CoalescingPolicy coalescingPolicy

The rule for when a change continues the step on top of the stack.

emptyStackIdback to summary
private long emptyStackId

The identity of the empty undo stack: the position the library stands at once everything still on the stack has been undone.

ORIGIN while nothing has been discarded. A change dropped by the LIMIT trim stays applied, so from then on an empty stack means "that change, and nothing after it": it takes the id of the change that fell off, and a save taken at exactly that position is recognised again when the user undoes their way back to it. clear is the other case — it discards history the library keeps, so no earlier position may match afterwards.

LIMITback to summary
private static final int LIMIT

Same depth javax.swing.undo.UndoManager defaulted to. The stack retains the BibEntry objects of removed entries, so an unbounded one keeps every deletion of a session alive.

listenersback to summary
private final List<Runnable> listeners

Notified after every change to either stack. Commands push from background tasks, so a listener that touches the UI is responsible for getting itself onto the right thread.

Copy-on-write rather than a plain list under the lock: registration happens a handful of times while the window is built, notification happens on every edit. Paying for a copy on the rare operation keeps the frequent one lock-free, and publishing through this list is what makes a listener registered on one thread visible to a push on another.

LOGGERback to summary
private static final Logger LOGGER
NEVER_SAVEDback to summary
private static final long NEVER_SAVED

A saved position no position will ever match, so the library counts as changed until it is saved again. markChanged stamps it; ids only ever grow from ORIGIN, so no push can reach it.

nextIdback to summary
private long nextId

Source of position ids. Only ever increments, so an id identifies one position for the lifetime of this manager: a position that history has discarded can never be matched by a later one that happens to sit at the same depth.

ORIGINback to summary
private static final long ORIGIN

The id of the empty stack before anything was ever pushed. Distinct from every id nextId hands out, so "nothing has been done yet" is a position like any other.

redoStackback to summary
private final Deque<JabRefUndoManager.UndoJournalEntry> redoStack
savedIdback to summary
private long savedId

The position the library was last saved at, as an id rather than a count. Compared by hasChanged.

stepFinishedback to summary
private boolean stepFinished

Whether the step on top of the stack is finished, so that the next change starts a new one rather than continuing it. Guarded by this object's monitor.

Set by anything that ends a user action — a recorded block, an undo, a redo, a caller saying so through endStep — and cleared by a single change, which the next one may still continue.

suspensionsback to summary
private final Set<JabRefUndoManager.Suspension> suspensions

The commands currently applying changes they have not yet handed over, in the order they started. Guarded by this object's monitor.

A collection rather than a flag: two background commands can write to one library at the same time, and undo may only return once both have handed over. Ordered, so the message names the command the user has been waiting on longest — and one that has finished is no longer in it to be named.

undoStackback to summary
private final Deque<JabRefUndoManager.UndoJournalEntry> undoStack

Constructor Detail

JabRefUndoManagerback to summary
public JabRefUndoManager()
JabRefUndoManagerback to summary
public JabRefUndoManager(CoalescingPolicy coalescingPolicy)

For tests that want every change to be its own step, or a rule of their own.

Method Detail

addEditback to summary
public void addEdit(BibChange change)

Implements org.jabref.logic.undo.UndoManager.addEdit.

Records a single change as its own undo step, or as part of the enclosing step when called inside addEdit.

A lone change needs no group and therefore no name: a ChangeSet exists to hold several changes together, and its name describes that grouping to the user.

Annotations
@Override
addEditback to summary
public boolean addEdit(String name, Consumer<CompoundEdit> mutations)

Implements org.jabref.logic.undo.UndoManager.addEdit.

Runs mutations, recording whatever it reports, and pushes the result as one undo step named name.

Nothing is pushed if no change was recorded, so callers need not check first — which is the point: a command cannot collect changes and then forget to hand them over.

A nested call becomes a nested ChangeSet inside its caller's set rather than a second undo step, so one user action stays one undo step even when a command delegates.

A block that fails part-way through still hands over what it managed to change. The library is modified either way at that point, and the difference between the two paths is only whether the user can take it back. The failure itself is not swallowed: it propagates once the step is on the stack. This holds for anything thrown, an Error included — the library does not become less modified because the failure was severe.

Returns:boolean

whether anything was recorded, for callers that report the outcome to the user

Annotations
@Override
addListenerback to summary
public void addListener(Runnable listener)

Registers a listener, from any thread and at any time — including from inside another listener, since notifyListeners iterates a snapshot.

applyEditback to summary
public ApplyResult applyEdit(BibChange change, EditSource source)

Implements org.jabref.logic.undo.UndoManager.applyEdit.

Performs change and records it in one go, so that the write to the library and the undo step describing it cannot drift apart — neither at the call site, which cannot do one without the other, nor in time, since both happen under one acquisition of the monitor.

The counterpart to addEdit(BibChange), and the difference between the two is only who performs the change: addEdit takes a change the caller has already made, this one makes it. Everything after that is the same journal entry.

The single acquisition is the point. Applying and then recording as two operations leaves a window in which the library holds the change and the journal does not: an undo arriving there reverts the previous change, and the history that ends up on the stack describes a library state that never existed. addEdit cannot offer this — by the time it hears about the change, the caller made it long ago.

Inside an addEdit block the change still reaches the library at once while the step reaches the stack only when the block ends; the block holds the library against undo for that whole window instead.

Returns:ApplyResult

what was applied, and what was not — see BibChange#apply

Annotations
@Override
applyingback to summary
private ApplyResult applying(Supplier<ApplyResult> apply)
canRedoback to summary
public synchronized boolean canRedo()
canUndoback to summary
public synchronized boolean canUndo()

Whether there is a step to take back — the stack only, not whether a command is holding the library.

Menu enablement binds to this, and a disabled menu item swallows its accelerator, so answering false during a suspension would leave Ctrl+Z doing nothing at all. undo enforces the suspension, and suspendedBy says which command to name.

clearback to summary
public void clear()
currentPositionback to summary
private synchronized long currentPosition()

The identity of the position the history is at: the change on top of the undo stack, or emptyStackId when everything has been undone.

endStepback to summary
public void endStep()

Implements org.jabref.logic.undo.UndoManager.endStep.

Ends the step being collected, so that the next change starts one of its own.

The journal cannot see where one user action ends and the next begins — a field editor records one change per keystroke either way — so the caller that knows says so. Ending a step nobody is continuing does nothing.

Annotations
@Override
hasChangedback to summary
public synchronized boolean hasChanged()

Whether the library differs from the last saved position.

Compares which position the history is at, not how far it has travelled. A count cannot answer this: pushing a change clears the redo stack, so two different histories reach the same count — save, undo, then make a different edit, and an edit balance is back where it started although the library now holds something that was never saved. That reported a modified library as saved, and org.jabref.gui.LibraryTab#requestClose closes such a library without offering to save it.

Ids are never reused, so a saved position discarded by a redo-stack clear can never be matched again — correct, because the library can no longer be brought back to it. A position dropped by the LIMIT trim is the opposite case: the change stays applied, so undoing everything that remains lands exactly on it, and emptyStackId carries its identity for that reason.

isApplyingback to summary
public boolean isApplying()

Whether this journal is applying a change on this thread right now.

For listeners that a model write reaches synchronously, on the applying thread, while undo or redo is between reading the stack and moving the entry across: recording anything from there clears the redo stack under the operation that is still using it. A thread-local answers exactly that question — the event is posted inside apply, on this thread, before it returns — and it is not a substitute for a change knowing where it came from, which is what org.jabref.model.entry.event.EntriesEventSource is for.

isEmptyStepback to summary
private static boolean isEmptyStep(BibChange change)

Whether change would be an undo step that does nothing.

A set that collected nothing is not an undo step: pushing one would enable Undo and let the next Ctrl+Z consume a no-op instead of the user's previous edit. Applying one changes nothing either, so both entry points skip it for the same reason.

isNoOpback to summary
private static boolean isNoOp(BibChange change)

Whether a merged run ended where it started.

markChangedback to summary
public void markChanged()

Implements org.jabref.logic.undo.UndoManager.markChanged.

Marks the library as changed by something this journal cannot take back — a migration on load, a shared-database update, a setting written without being recorded.

The stack is left alone; what moves is the saved position, to one no position can equal. Undoing every step therefore no longer means "back to what was saved", which is the truth: the change is still there. Saving is what clears it.

markUnchangedback to summary
public void markUnchanged()

Marks the current position as saved.

Notifies, like every other move of the saved position: a listener deriving the modified marker from hasChanged has to hear about the one moment the answer turns false.

notifyListenersback to summary
private void notifyListeners()

Runs every listener even if one of them throws. By the time this is called the change is on the stack and applied to the library, so letting a listener's failure out would report a completed operation as failed — and would silently deprive the remaining listeners of a notification they have no other way to get.

pushback to summary
private void push(BibChange change)

Puts change on the undo stack as a new position and discards the redo stack, which the new change has made unreachable.

Callers hold this object's monitor: the stack transition is what the monitor protects, and applyEdit needs the model write to happen inside the same acquisition.

recordback to summary
private void record(BibChange change, EditSource source)

Records change, either as a step of its own or as the continuation of the step on top of the stack — see CoalescingPolicy.

A merged step takes a new position id, because the library is somewhere it has not been before: keeping the old id would let a save taken mid-run report the library as unchanged after the rest of the run was typed. For the same reason a step is never merged into while it is the saved position: the user would lose the saved state from the stack, and undoing once would take back more than they typed since saving.

A run that ends where it started leaves nothing to undo — typing a word and deleting it again — so the step goes rather than sitting on the stack doing nothing.

Callers hold this object's monitor.

redoback to summary
public Optional<UndoStep> redo()
Returns:Optional<UndoStep>

what was redone, in the shape undo returns it, or empty if there was nothing to redo or a command is holding the library

suspendedByback to summary
public synchronized Optional<String> suspendedBy()

The command currently holding this library, if one is: of those still writing, the one that started first.

For the Undo and Redo actions, which have to tell "nothing to undo" from "not while this is running" — both of which make canUndo false.

suspendUndoback to summary
public UndoSuspension suspendUndo(String name)

Implements org.jabref.logic.undo.UndoManager.suspendUndo.

Suspends undo and redo for this library until the returned suspension is closed.

Opening and closing are both stack changes as far as observers are concerned — enablement has to fall while a command holds the library and rise again afterwards — so both notify, and both do so after the monitor is released.

Annotations
@Override
undoback to summary
public Optional<UndoStep> undo()

Applies the inverse before moving the change across, so a change that throws stays undoable instead of vanishing from both stacks.

A change that refuses — because the library no longer holds what it recorded — does move across: the step was consumed, retrying it would refuse again, and the caller is told through the result that not all of it applied.

Returns:Optional<UndoStep>

what was undone — its name for the user, and what of it could not be applied — or empty if there was nothing to undo, or a command is holding the library (see suspendUndo; suspendedBy tells the two apart). The name is taken inside the monitor, and it is all that leaves the journal, so nothing outside reads the stacks.

org.jabref.logic.undo back to summary

private final Class JabRefUndoManager.Suspension

extends Object
implements UndoSuspension
Class Inheritance
All Implemented Interfaces
org.jabref.logic.undo.UndoSuspension, java.lang.AutoCloseable

One command's claim on this library, ended by close.

Field Summary

Modifier and TypeField and Description
private boolean
private final String

Constructor Summary

AccessConstructor and Description
private

Method Summary

Modifier and TypeMethod and Description
public void
close()

Implements org.jabref.logic.undo.UndoSuspension.close.

Ends the suspension.

private String
name()

Inherited from java.lang.Object:
cloneequalsfinalizegetClasshashCodenotifynotifyAlltoStringwaitwaitwait

Field Detail

closedback to summary
private boolean closed
nameback to summary
private final String name

Constructor Detail

Suspensionback to summary
private Suspension(String name)

Method Detail

closeback to summary
public void close()

Implements org.jabref.logic.undo.UndoSuspension.close.

Doc from org.jabref.logic.undo.UndoSuspension.close.

Ends the suspension. Never throws, so it is safe in a finally and in try-with-resources.

Annotations
@Override
nameback to summary
private String name()
org.jabref.logic.undo back to summary

private final Record JabRefUndoManager.UndoJournalEntry

extends Record
Class Inheritance
Record Components
id:long 
change:BibChange 

A change together with the identity of the position it occupies in the history.

The id lives here rather than on the change because a BibChange is a value describing a modification, and a position in this journal is bookkeeping that only this class needs. Putting it on the change would also cost inverted() its involution: it would have to either copy the id, giving two distinct positions the same identity, or drop it.

Field Summary

Modifier and TypeField and Description
private final BibChange
change

Record Component accessed by change().

private final long
id

Record Component accessed by id().

Constructor Summary

AccessConstructor and Description
private
UndoJournalEntry(long id, BibChange change)

Method Summary

Modifier and TypeMethod and Description
public BibChange
change()

Record Component getter of change.

public final boolean
equals(Object o)

Implements abstract java.lang.Record.equals.

Indicates whether some other object is "equal to" this one.

public final int
hashCode()

Implements abstract java.lang.Record.hashCode.

Returns a hash code value for the record.

public long
id()

Record Component getter of id.

public final String
toString()

Implements abstract java.lang.Record.toString.

Returns a string representation of the record.

Field Detail

changeback to summary
private final BibChange change

Record Component accessed by change().

idback to summary
private final long id

Record Component accessed by id().

Constructor Detail

UndoJournalEntryback to summary
private UndoJournalEntry(long id, BibChange change)

Method Detail

changeback to summary
public BibChange change()

Record Component getter of change.

equalsback to summary
public final boolean equals(Object o)

Implements abstract java.lang.Record.equals.

Doc from java.lang.Record.equals.

Indicates whether some other object is "equal to" this one. In addition to the general contract of Object.equals, record classes must further obey the invariant that when a record instance is "copied" by passing the result of the record component accessor methods to the canonical constructor, as follows:

    R copy = new R(r.c1(), r.c2(), ..., r.cn());
then it must be the case that r.equals(copy).
hashCodeback to summary
public final int hashCode()

Implements abstract java.lang.Record.hashCode.

Doc from java.lang.Record.hashCode.

Returns a hash code value for the record. Obeys the general contract of Object.hashCode. For records, hashing behavior is constrained by the refined contract of Record.equals, so that any two records created from the same components must have the same hash code.

idback to summary
public long id()

Record Component getter of id.

toStringback to summary
public final String toString()

Implements abstract java.lang.Record.toString.

Doc from java.lang.Record.toString.

Returns a string representation of the record. In accordance with the general contract of Object#toString(), the toString method returns a string that "textually represents" this record. The result should be a concise but informative representation that is easy for a person to read.

In addition to this general contract, record classes must further participate in the invariant that any two records which are equal must produce equal strings. This invariant is necessarily relaxed in the rare case where corresponding equal component values might fail to produce equal strings for themselves.