diff --git a/editor.lib/src/org/netbeans/editor/BaseDocument.java b/editor.lib/src/org/netbeans/editor/BaseDocument.java --- a/editor.lib/src/org/netbeans/editor/BaseDocument.java +++ b/editor.lib/src/org/netbeans/editor/BaseDocument.java @@ -73,6 +73,7 @@ import javax.swing.text.BadLocationException; import javax.swing.text.DefaultEditorKit; import javax.swing.text.Document; +import javax.swing.text.DocumentFilter; import javax.swing.text.Position; import javax.swing.text.Element; import javax.swing.text.AttributeSet; @@ -336,6 +337,8 @@ private DocumentListener postModificationDocumentListener; + private DocumentEvent postModificationEvent; + private ListenerList postModificationDocumentListenerList = new ListenerList(); private ListenerList updateDocumentListenerList = new ListenerList(); @@ -352,6 +355,8 @@ private UndoableEdit removeUpdateLineUndo; + private DocumentFilter.FilterBypass filterBypass; + private Preferences prefs; private final PreferenceChangeListener prefsListener = new PreferenceChangeListener() { public @Override void preferenceChange(PreferenceChangeEvent evt) { @@ -724,7 +729,7 @@ } /** Inserts string into document */ - public @Override void insertString(int offset, String text, AttributeSet a) + public @Override void insertString(int offset, String text, AttributeSet attrs) throws BadLocationException { if (LOG_EDT.isLoggable(Level.FINE)) { // Only permit operations in EDT if (!SwingUtilities.isEventDispatchThread()) { @@ -733,115 +738,28 @@ } } - if (text == null || text.length() == 0) { - return; - } - - // Check offset correctness - if (offset < 0 || offset > getLength()) { - throw new BadLocationException("Wrong insert position " + offset, offset); // NOI18N - } - - // possible CR-LF conversion - text = ReadWriteUtils.convertToNewlines(text); - + boolean notifyMod = notifyModifyCheckStart(offset, "insertString() vetoed"); // NOI18N + boolean modFinished = false; // Whether modification succeeded // Check whether there is an active postModificationDocumentListener // and if so then start an atomic transaction. boolean activePostModification; - DocumentEvent postModificationEvent = null; synchronized (this) { activePostModification = (postModificationDocumentListener != null || postModificationDocumentListenerList.getListenerCount() > 0); if (activePostModification) { atomicLockImpl (); + } else { + extWriteLock(); } } try { - - // Perform the insert - boolean notifyMod = notifyModifyCheckStart(offset, "insertString() vetoed"); // NOI18N - boolean modFinished = false; // Whether modification succeeded - extWriteLock(); - try { - incrementDocVersion(); - - /* - boolean checkSpaces = inited && org.netbeans.lib.editor.util.swing.DocumentUtilities.isTypingModification(this); - if (checkSpaces) { - Position offsPosition = createPosition(offset); - checkTrailingSpaces(offset); - offset = offsPosition.getOffset(); - } - */ - - preInsertCheck(offset, text, a); - - // Do the real insert into the content - UndoableEdit edit = getContent().insertString(offset, text); - - /* - if (checkSpaces) { - lastPositionEditedByTyping = createPosition(offset); - } - */ - - if (LOG.isLoggable(Level.FINE)) { - String msg = "BaseDocument.insertString(): doc=" + this // NOI18N - + (modified ? "" : " - first modification") // NOI18N - + ", offset=" + Utilities.offsetToLineColumnString(this, offset) // NOI18N - + (debugNoText ? "" : (", text='" + text + "'")); // NOI18N - - if (debugStack) { - LOG.log(Level.FINE, msg, new Throwable(msg)); - } else { - LOG.log(Level.FINE, msg); - } - } - - BaseDocumentEvent evt = getDocumentEvent(offset, text.length(), DocumentEvent.EventType.INSERT, a); - - preInsertUpdate(evt, a); - - if (edit != null) { - evt.addEdit(edit); - - lastModifyUndoEdit = edit; // #8692 check last modify undo edit - } - - modified = true; - - if (atomicDepth > 0) { - ensureAtomicEditsInited(); - atomicEdits.addEdit(evt); // will be added - } - - insertUpdate(evt, a); - - evt.end(); - - fireInsertUpdate(evt); - - boolean isComposedText = ((a != null) - && (a.isDefined(StyleConstants.ComposedTextAttribute))); - - if (composedText && !isComposedText) - composedText = false; - if (!composedText && isComposedText) - composedText = true; - - if (atomicDepth == 0 && !isComposedText) { // !!! check - fireUndoableEditUpdate(new UndoableEditEvent(this, evt)); + DocumentFilter filter = getDocumentFilter(); + if (filter != null) { + filter.insertString(getFilterBypass(), offset, text, attrs); + } else { + handleInsertString(offset, text, attrs); } modFinished = true; - postModificationEvent = evt; - } finally { - extWriteUnlock(); - // Notify no mod done if notified mod but mod did not succeeded - if (notifyMod) { - notifyModifyCheckEnd(modFinished); - } - } - } finally { // for post modification if (activePostModification) { try { @@ -856,9 +774,87 @@ } finally { atomicUnlockImpl(); } + } else { + extWriteUnlock(); + } + // Notify no mod done if notified mod but mod did not succeeded + if (notifyMod) { + notifyModifyCheckEnd(modFinished); } } } + + void handleInsertString(int offset, String text, AttributeSet attrs) throws BadLocationException { + if (text == null || text.length() == 0) { + return; + } + + // Check offset correctness + if (offset < 0 || offset > getLength()) { + throw new BadLocationException("Wrong insert position " + offset, offset); // NOI18N + } + + // possible CR-LF to LF conversion + text = ReadWriteUtils.convertToNewlines(text); + + postModificationEvent = null; + incrementDocVersion(); + + preInsertCheck(offset, text, attrs); + + UndoableEdit edit = getContent().insertString(offset, text); + + if (LOG.isLoggable(Level.FINE)) { + String msg = "BaseDocument.insertString(): doc=" + this // NOI18N + + (modified ? "" : " - first modification") // NOI18N + + ", offset=" + Utilities.offsetToLineColumnString(this, offset) // NOI18N + + (debugNoText ? "" : (", text='" + text + "'")); // NOI18N + + if (debugStack) { + LOG.log(Level.FINE, msg, new Throwable(msg)); + } else { + LOG.log(Level.FINE, msg); + } + } + + BaseDocumentEvent evt = getDocumentEvent(offset, text.length(), DocumentEvent.EventType.INSERT, attrs); + + preInsertUpdate(evt, attrs); + + if (edit != null) { + evt.addEdit(edit); + + lastModifyUndoEdit = edit; // #8692 check last modify undo edit + } + + modified = true; + + if (atomicDepth > 0) { + ensureAtomicEditsInited(); + atomicEdits.addEdit(evt); // will be added + } + + insertUpdate(evt, attrs); + + evt.end(); + + fireInsertUpdate(evt); + + boolean isComposedText = ((attrs != null) + && (attrs.isDefined(StyleConstants.ComposedTextAttribute))); + + if (composedText && !isComposedText) { + composedText = false; + } + if (!composedText && isComposedText) { + composedText = true; + } + + if (atomicDepth == 0 && !isComposedText) { // !!! check + fireUndoableEditUpdate(new UndoableEditEvent(this, evt)); + } + postModificationEvent = evt; + } public void checkTrailingSpaces(int offset) { try { @@ -896,106 +892,39 @@ } /** Removes portion of a document */ - public @Override void remove(int offset, int len) throws BadLocationException { + public @Override void remove(int offset, int length) throws BadLocationException { if (LOG_EDT.isLoggable(Level.FINE)) { // Only permit operations in EDT if (!SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("BaseDocument.insertString not in EDT: offset=" + // NOI18N - offset + ", len=" + len); // NOI18N + offset + ", len=" + length); // NOI18N } } - if (len > 0) { - if (offset < 0) { - throw new BadLocationException("Wrong remove position " + offset, offset); // NOI18N + boolean notifyMod = notifyModifyCheckStart(offset, "remove() vetoed"); // NOI18N + boolean modFinished = false; // Whether modification succeeded + // Check whether there is an active postModificationDocumentListener + // and if so then start an atomic transaction. + boolean activePostModification; + synchronized (this) { + activePostModification = (postModificationDocumentListener != null + || postModificationDocumentListenerList.getListenerCount() > 0); + if (activePostModification) { + atomicLockImpl (); + } else { + extWriteLock(); } - - // Check whether there is an active postModificationDocumentListener - // and if so then start an atomic transaction. - boolean activePostModification; - DocumentEvent postModificationEvent = null; - synchronized (this) { - activePostModification = (postModificationDocumentListener != null - || postModificationDocumentListenerList.getListenerCount() > 0); - if (activePostModification) { - atomicLockImpl (); - } + } + try { + DocumentFilter filter = getDocumentFilter(); + if (filter != null) { + filter.remove(getFilterBypass(), offset, length); + } else { + handleRemove(offset, length); } - try { - - boolean notifyMod = notifyModifyCheckStart(offset, "remove() vetoed"); // NOI18N - boolean modFinished = false; // Whether modification succeeded - extWriteLock(); - try { - incrementDocVersion(); - - int docLen = getLength(); - if (offset < 0 || offset > docLen) { - throw new BadLocationException("Wrong remove position " + offset, offset); // NOI18N - } - if (offset + len > docLen) { - throw new BadLocationException("End offset of removed text " // NOI18N - + (offset + len) + " > getLength()=" + docLen, // NOI18N - offset + len - ); // NOI18N - } - - preRemoveCheck(offset, len); - - BaseDocumentEvent evt = getDocumentEvent(offset, len, DocumentEvent.EventType.REMOVE, null); - // Store modification text as an event's property - org.netbeans.lib.editor.util.swing.DocumentUtilities.addEventPropertyStorage(evt); - String removedText = getText(offset, len); - org.netbeans.lib.editor.util.swing.DocumentUtilities.putEventProperty(evt, String.class, removedText); - - removeUpdate(evt); - - UndoableEdit edit = ((EditorDocumentContent)getContent()).remove(offset, removedText); - if (edit != null) { - evt.addEdit(edit); - - lastModifyUndoEdit = edit; // #8692 check last modify undo edit - } - - if (LOG.isLoggable(Level.FINE)) { - String msg = "BaseDocument.remove(): doc=" + this // NOI18N - + ", origDocLen=" + docLen // NOI18N - + ", offset=" + Utilities.offsetToLineColumnString(this, offset) // NOI18N - + ", len=" + len // NOI18N - + (debugNoText ? "" : (", removedText='" + ((ContentEdit)edit).getText() + "'")); //NOI18N - - if (debugStack) { - LOG.log(Level.FINE, msg, new Throwable(msg)); - } else { - LOG.log(Level.FINE, msg); - } - } - - if (atomicDepth > 0) { // add edits as soon as possible - ensureAtomicEditsInited(); - atomicEdits.addEdit(evt); // will be added - } - - postRemoveUpdate(evt); - - evt.end(); - - fireRemoveUpdate(evt); - if (atomicDepth == 0 && !composedText) { - fireUndoableEditUpdate(new UndoableEditEvent(this, evt)); - } - - modFinished = true; - postModificationEvent = evt; - } finally { - extWriteUnlock(); - // Notify no mod done if notified mod but mod did not succeeded - if (notifyMod) { - notifyModifyCheckEnd(modFinished); - } - } - - } finally { // for post modification - if (activePostModification) { + modFinished = true; + } finally { // for post modification + if (activePostModification) { + try { if (postModificationEvent != null) { // Modification finished successfully if (postModificationDocumentListener != null) { postModificationDocumentListener.removeUpdate(postModificationEvent); @@ -1004,11 +933,147 @@ listener.removeUpdate(postModificationEvent); } } + } finally { atomicUnlockImpl (); } + } else { + extWriteUnlock(); } + // Notify no mod done if notified mod but mod did not succeeded + if (notifyMod) { + notifyModifyCheckEnd(modFinished); + } + } + } + + void handleRemove(int offset, int length) throws BadLocationException { + if (length == 0) { + return; + } + if (length < 0) { + throw new IllegalArgumentException("len=" + length + " < 0"); // NOI18N + } + if (offset < 0) { + throw new BadLocationException("Wrong remove position " + offset + " < 0", offset); // NOI18N + } + if (offset + length > getLength()) { + throw new BadLocationException("Wrong (offset+length)=" + (offset+length) + + " > getLength()=" + getLength(), offset + length); // NOI18N + } + postModificationEvent = null; + incrementDocVersion(); + + int docLen = getLength(); + if (offset < 0 || offset > docLen) { + throw new BadLocationException("Wrong remove position " + offset, offset); // NOI18N } + if (offset + length > docLen) { + throw new BadLocationException("End offset of removed text " // NOI18N + + (offset + length) + " > getLength()=" + docLen, // NOI18N + offset + length); // NOI18N + } + + preRemoveCheck(offset, length); + + BaseDocumentEvent evt = getDocumentEvent(offset, length, DocumentEvent.EventType.REMOVE, null); + // Store modification text as an event's property + org.netbeans.lib.editor.util.swing.DocumentUtilities.addEventPropertyStorage(evt); + String removedText = getText(offset, length); + org.netbeans.lib.editor.util.swing.DocumentUtilities.putEventProperty(evt, String.class, removedText); + + removeUpdate(evt); + + UndoableEdit edit = ((EditorDocumentContent) getContent()).remove(offset, removedText); + if (edit != null) { + evt.addEdit(edit); + + lastModifyUndoEdit = edit; // #8692 check last modify undo edit + } + + if (LOG.isLoggable(Level.FINE)) { + String msg = "BaseDocument.remove(): doc=" + this // NOI18N + + ", origDocLen=" + docLen // NOI18N + + ", offset=" + Utilities.offsetToLineColumnString(this, offset) // NOI18N + + ", len=" + length // NOI18N + + (debugNoText ? "" : (", removedText='" + ((ContentEdit) edit).getText() + "'")); //NOI18N + + if (debugStack) { + LOG.log(Level.FINE, msg, new Throwable(msg)); + } else { + LOG.log(Level.FINE, msg); + } + } + + if (atomicDepth > 0) { // add edits as soon as possible + ensureAtomicEditsInited(); + atomicEdits.addEdit(evt); // will be added + } + + postRemoveUpdate(evt); + + evt.end(); + + fireRemoveUpdate(evt); + if (atomicDepth == 0 && !composedText) { + fireUndoableEditUpdate(new UndoableEditEvent(this, evt)); + } + postModificationEvent = evt; + } + + public void replace(int offset, int length, String text, AttributeSet attrs) throws BadLocationException { + boolean notifyMod = notifyModifyCheckStart(offset, "replace() vetoed"); // NOI18N + boolean modFinished = false; // Whether modification succeeded + // Check whether there is an active postModificationDocumentListener + // and if so then start an atomic transaction. + boolean activePostModification; + synchronized (this) { + activePostModification = (postModificationDocumentListener != null + || postModificationDocumentListenerList.getListenerCount() > 0); + if (activePostModification) { + atomicLockImpl (); + } else { + extWriteLock(); + } + } + try { + DocumentFilter filter = getDocumentFilter(); + if (filter != null) { + filter.replace(getFilterBypass(), offset, length, text, attrs); + } else { + handleRemove(offset, length); + handleInsertString(offset, text, attrs); + } + modFinished = true; + } finally { // for post modification + if (activePostModification) { + try { + if (postModificationEvent != null) { // Modification finished successfully + if (postModificationDocumentListener != null) { + postModificationDocumentListener.insertUpdate(postModificationEvent); + } + for (DocumentListener listener: postModificationDocumentListenerList.getListeners()) { + listener.insertUpdate(postModificationEvent); + } + } + } finally { + atomicUnlockImpl(); + } + } else { + extWriteUnlock(); + } + // Notify no mod done if notified mod but mod did not succeeded + if (notifyMod) { + notifyModifyCheckEnd(modFinished); + } + } + } + + private DocumentFilter.FilterBypass getFilterBypass() { + if (filterBypass == null) { + filterBypass = new FilterBypassImpl(); + } + return filterBypass; } /** @@ -1672,6 +1737,10 @@ ((UndoableEditListener)listeners[i + 1]).undoableEditHappened(e); } } + + // Since UndoManager may only do um.addEdit() the original resetting + // in AtomicCompoundEdit.replaceEdit() would not be called in such case. + undoMergeReset = false; } /** Extended write locking of the document allowing @@ -2301,7 +2370,6 @@ } } } - undoMergeReset = false; return false; } @@ -2538,4 +2606,23 @@ } } // End of PlainEditorKit class + class FilterBypassImpl extends DocumentFilter.FilterBypass { + + public Document getDocument() { + return BaseDocument.this; + } + + public void remove(int offset, int length) throws BadLocationException { + handleRemove(offset, length); + } + + public void insertString(int offset, String string, AttributeSet attrs) throws BadLocationException { + handleInsertString(offset, string, attrs); + } + + public void replace(int offset, int length, String text, AttributeSet attrs) throws BadLocationException { + handleRemove(offset, length); + handleInsertString(offset, text, attrs); + } + } } diff --git a/openide.awt/src/org/openide/awt/UndoRedo.java b/openide.awt/src/org/openide/awt/UndoRedo.java --- a/openide.awt/src/org/openide/awt/UndoRedo.java +++ b/openide.awt/src/org/openide/awt/UndoRedo.java @@ -139,91 +139,40 @@ private final ChangeSupport cs = new ChangeSupport(this); - /** vector of Edits to run */ - private final LinkedList runus = new LinkedList(); // for fix of #8692 - - /** Called from undoableEditHappened() inner class */ - private void superUndoableEditHappened(UndoableEditEvent ue) { - super.undoableEditHappened(ue); - } - - /** Called from discardAllEdits() inner class */ - private void superDiscardAllEdits() { - super.discardAllEdits(); - } - /** Consume an undoable edit. * Delegates to superclass and notifies listeners. * @param ue the edit */ @Override public void undoableEditHappened(final UndoableEditEvent ue) { - /* Edits are posted to request processor and the deadlock - * in #8692 between undoredo and document that fires - * the undoable edit should be avoided this way. - */ - synchronized (runus) { - runus.add(ue); - } - - updateTask(); + super.undoableEditHappened(ue); + cs.fireChange(); } /** Discard all the existing edits from the undomanager. */ @Override public void discardAllEdits() { - synchronized (runus) { - runus.add(null); - } - - updateTask(); + super.discardAllEdits(); + cs.fireChange(); } @Override public void undo() throws CannotUndoException { super.undo(); - updateTask(); + cs.fireChange(); } @Override public void redo() throws CannotRedoException { super.redo(); - updateTask(); + cs.fireChange(); } @Override public void undoOrRedo() throws CannotRedoException, CannotUndoException { - super.undoOrRedo(); - updateTask(); + super.undoOrRedo(); // cs.fireChange() either in undo() or redo() } - private void updateTask() { - for (;;) { - UndoableEditEvent ue; - - synchronized (runus) { - if (runus.isEmpty()) { - break; - } - - ue = runus.removeFirst(); - } - - if (ue == null) { - superDiscardAllEdits(); - } else { - superUndoableEditHappened(ue); - } - } - cs.fireChange(); - } - - /* Attaches change listener to the this object. - * The listener is notified everytime the undo/redo - * ability of this object changes. - */ - - //#32313 - synchronization of this method was removed @Override public void addChangeListener(ChangeListener l) { cs.addChangeListener(l); diff --git a/openide.text/src/org/openide/text/CloneableEditorSupport.java b/openide.text/src/org/openide/text/CloneableEditorSupport.java --- a/openide.text/src/org/openide/text/CloneableEditorSupport.java +++ b/openide.text/src/org/openide/text/CloneableEditorSupport.java @@ -215,7 +215,7 @@ private Listener listener; /** the undo/redo manager to use for this document */ - private UndoRedo.Manager undoRedo; + private UndoRedoManager undoRedo; /** lines set for this object */ private Line.Set lineSet; @@ -261,8 +261,7 @@ *
* Also set when document is being reloaded. */ - private boolean revertingUndoOrReloading; - private boolean justRevertedToNotModified; + private boolean documentReloading; private volatile int documentStatus = DOCUMENT_NO; private Throwable prepareDocumentRuntimeException; @@ -271,10 +270,12 @@ */ private Map> lineSetWHM; private boolean annotationsLoaded; + + private DocFilter docFilter; /** Classes that have been warned about overriding asynchronousOpen() */ private static final Set warnedClasses = new WeakSet(); - + /** Creates new CloneableEditorSupport attached to given environment. * * @param env environment that is source of all actions around the @@ -385,11 +386,22 @@ } if (undoRedo == null) { - undoRedo = createUndoRedoManager(); + UndoRedo.Manager mgr = createUndoRedoManager(); + if (!(mgr instanceof UndoRedoManager)) { + ERR.info("createUndoRedoManager(): ignoring created instance of class " + // NOI18N + mgr.getClass() + " since CloneableEditorSupport requires instance of " + // NOI18N" + UndoRedoManager.class.getName() + "\n"); // NOI18N + mgr = new UndoRedoManager(this); + } + undoRedo = (UndoRedoManager) mgr; } return undoRedo; } + + UndoRedoManager getUndoRedoManager() { + return (UndoRedoManager) getUndoRedo(); + } /** Provides access to position manager for the document. * It maintains a set of positions even the document is in memory @@ -728,6 +740,7 @@ // atomic action has finished // definitively sooner than leaving lock section // and notifying al waiters, see #47022 + getUndoRedoManager().markSavepoint(); getDoc().addUndoableEditListener(getUndoRedo()); d = getDoc(); } catch (DelegateIOExc t) { @@ -782,12 +795,33 @@ } else { d.putProperty("modificationListener", null); // NOI18N } - } else { - if (add) { - d.addDocumentListener(getListener()); - } else { - d.removeDocumentListener(getListener()); + } + + if (add) { + if (d instanceof AbstractDocument) { + AbstractDocument aDoc = (AbstractDocument) d; + DocumentFilter origFilter = aDoc.getDocumentFilter(); + docFilter = new DocFilter(origFilter); + aDoc.setDocumentFilter(docFilter); + } else { // Put property for non-AD + DocumentFilter origFilter = (DocumentFilter) d.getProperty(DocumentFilter.class); + docFilter = new DocFilter(origFilter); + d.putProperty(DocumentFilter.class, docFilter); } + d.addDocumentListener(getListener()); + + + } else { // remove filter + if (docFilter != null) { + if (d instanceof AbstractDocument) { + AbstractDocument aDoc = (AbstractDocument) d; + aDoc.setDocumentFilter(docFilter.origFilter); + } else { // Put property for non-AD + d.putProperty(DocumentFilter.class, docFilter.origFilter); + } + docFilter = null; + } + d.removeDocumentListener(getListener()); } } @@ -1076,8 +1110,7 @@ } } - // Insert before-save undo event to enable unmodifying undo - getUndoRedo().undoableEditHappened(new UndoableEditEvent(this, new BeforeSaveEdit(lastSaveTime))); + getUndoRedoManager().markSavepoint(); // update cached info about lines updateLineSet(true); @@ -1101,7 +1134,12 @@ // Run before-save actions Runnable beforeSaveRunnable = (Runnable) myDoc.getProperty("beforeSaveRunnable"); if (beforeSaveRunnable != null) { - beforeSaveRunnable.run(); + undoRedo.setPerformingSaveActions(true); + try { + beforeSaveRunnable.run(); + } finally { + undoRedo.setPerformingSaveActions(false); + } } SaveAsReader saveAsReader = new SaveAsReader(); @@ -1505,14 +1543,14 @@ /** Test whether the document is ready. * @return true if document is ready */ - private boolean isDocumentReady() { + boolean isDocumentReady() { CloneableEditorSupport redirect = CloneableEditorSupportRedirector.findRedirect(this); if (redirect != null) { return redirect.isDocumentReady(); } return documentStatus == DOCUMENT_READY; } - + /** * Set the MIME type for the document. * @param s the new MIME type @@ -1600,7 +1638,7 @@ * @return the undo/redo manager */ protected UndoRedo.Manager createUndoRedoManager() { - return new CESUndoRedoManager(this); + return new UndoRedoManager(this); } /** Returns an InputStream which reads the current data from this editor, taking into @@ -1808,6 +1846,7 @@ // XXX do this from AWT??? ERR.fine("task-discardAllEdits"); getUndoRedo().discardAllEdits(); + getUndoRedoManager().markSavepoint(); ERR.fine("task-check already modified"); // #57104 - if modified previously now it should become unmodified if (isAlreadyModified()) { @@ -1925,30 +1964,11 @@ * @return true if the modification was allowed, false if it should be prohibited */ final boolean callNotifyModified() { - // #57104 - when reverting undo the revertingUndoOrReloading flag is set - // to prevent infinite undoing which could happen now due to fix #56963 - // (undoable edit being undone in the document notifies - // document's modification listener to mark the file as modified). - // Maybe clearing alreadyModified flag - // AFTER revertPreviousOrUpcomingUndo() could suffice as well - // instead of the revertingUndoOrReloading flag. - // Also notifyModified() is not called during reloadDocument() - // to prevent situation when output stream is taken from the file - // (for which CloneableEditorSupport exists) under file's lock - // and once closed (still under file's lock) the CES is trying to reload - // the file calling notifyModified() that tries to grab the lock - // and fails leading to undoing of the file's content to the one - // before the reload. - if (!isAlreadyModified() && !revertingUndoOrReloading) { + if (!isAlreadyModified() && !documentReloading) { setAlreadyModified(true); if (!notifyModified()) { - ERR.log(Level.INFO,"callNotifyModified notifyModified returns false this:" + getClass().getName()); setAlreadyModified(false); - revertingUndoOrReloading = true; - revertPreviousOrUpcomingUndo(); - revertingUndoOrReloading = false; - return false; } } @@ -2036,81 +2056,6 @@ return true; } - /** Resets listening on UndoRedo, - * and in case next undo edit comes, schedules processesing of it. - * Used to revert modification e.g. of document of [read-only] env. */ - private void revertPreviousOrUpcomingUndo() { - UndoRedo.Manager ur = getUndoRedo(); - Listener l = getListener(); - - if (Boolean.TRUE.equals(getDocument().getProperty("supportsModificationListener"))) { // NOI18N - - // revert undos now - SearchBeforeModificationEdit edit = new SearchBeforeModificationEdit(); - - try { - for (;;) { - edit.delegate = null; - ur.undoableEditHappened(new UndoableEditEvent(getDocument(), edit)); - - if (edit.delegate == null) break; // no previous edit - - if (edit.delegate instanceof BeforeModificationEdit) { - if (edit.delegate != null) { - // undo anyway - ur.undo(); - } - - // and exit - break; - } - - if (edit.delegate instanceof BeforeSaveEdit) { - break; - } - - // otherwise remove the edit - ur.undo(); - } - } catch (CannotUndoException ex) { - // ok, cannot undo, just ignore this - } - } else { - // revert upcomming undo - l.setUndoTask(new Runnable() { - public void run() { - undoAll(); - } - } - ); - ur.addChangeListener(l); - } - } - - /** Creates Runnable which tries to make one undo. Helper method. - * @see #revertUpcomingUndo */ - final void undoAll() { - StyledDocument sd = getDoc(); - - if (sd == null) { - // #20883, doc can be null(!), doCloseDocument was faster. - return; - } - - UndoRedo ur = getUndoRedo(); - addRemoveDocListener(sd, false); - - try { - if (ur.canUndo()) { - ur.undo(); - } - } catch (CannotUndoException cne) { - ERR.log(Level.INFO, null, cne); - } finally { - addRemoveDocListener(sd, true); - } - } - /** Method that is called when all components of the support are * closed. The default implementation closes the document. * @@ -2844,18 +2789,15 @@ /** The listener that this support uses to communicate with * document, environment and also temporarilly on undoredo. */ - private final class Listener extends Object implements ChangeListener, DocumentListener, PropertyChangeListener, + private final class Listener extends Object implements PropertyChangeListener, DocumentListener, Runnable, java.beans.VetoableChangeListener { + + /** Stores exception from loadDocument, can be set in run method */ + private IOException loadExc; + /** revert modification if asked */ private boolean revertModifiedFlag; - /** Stores exception from loadDocument, can be set in run method */ - private IOException loadExc; - - /** Stores temporarilly undo task for reverting prohibited changes. - * @see CloneableEditorSupport#createUndoTask */ - private Runnable undoTask; - Listener() { } @@ -2868,28 +2810,18 @@ // loadExc = null; return ret; } - - /** Sets undo task used to revert prohibited change. */ - public void setUndoTask(Runnable undoTask) { - this.undoTask = undoTask; + + public void insertUpdate(DocumentEvent evt) { + callNotifyModified(); + revertModifiedFlag = false; } - /** Schedules reverting(undoing) of prohibited change. - * Implements ChangeListener. - * @see #revertUpcomingUndo */ - public void stateChanged(ChangeEvent evt) { - getUndoRedo().removeChangeListener(this); - undoTask.run(); - - //SwingUtilities.invokeLater(undoTask); - undoTask = null; + public void removeUpdate(DocumentEvent evt) { + callNotifyModified(); + revertModifiedFlag = false; } - - /** Gives notification that an attribute or set of attributes changed. - * @param ev event describing the action - */ - public void changedUpdate(DocumentEvent ev) { - //modified(); (bugfix #1492) + + public void changedUpdate(DocumentEvent evt) { } public void vetoableChange(PropertyChangeEvent evt) @@ -2912,22 +2844,6 @@ } } - /** Gives notification that there was an insert into the document. - * @param ev event describing the action - */ - public void insertUpdate(DocumentEvent ev) { - callNotifyModified(); - revertModifiedFlag = false; - } - - /** Gives notification that a portion of the document has been removed. - * @param ev event describing the action - */ - public void removeUpdate(DocumentEvent ev) { - callNotifyModified(); - revertModifiedFlag = false; - } - /** Listener to changes in the Env. */ public void propertyChange(PropertyChangeEvent ev) { @@ -2963,9 +2879,9 @@ } // #57104 - avoid notifyModified() which takes file lock - revertingUndoOrReloading = true; + documentReloading = true; NbDocument.runAtomic(sd, this); - revertingUndoOrReloading = false; // #57104 + documentReloading = false; // #57104 return; } @@ -3022,9 +2938,6 @@ setLastSaveTime(cesEnv().getTime().getTime()); - // Insert before-save undo event to enable unmodifying undo - getUndoRedo().undoableEditHappened(new UndoableEditEvent(this, new BeforeSaveEdit(lastSaveTime))); - // Start listening on changes in document addRemoveDocListener(getDoc(), true); } @@ -3032,756 +2945,6 @@ // } } - /** Generic undoable edit that delegates to the given undoable edit. */ - private class FilterUndoableEdit - implements UndoableEdit, UndoGroupManager.SeparateEdit - { - protected UndoableEdit delegate; - - FilterUndoableEdit() { - } - - public void undo() throws CannotUndoException { - if (delegate != null) { - delegate.undo(); - } - } - - public boolean canUndo() { - if (delegate != null) { - return delegate.canUndo(); - } else { - return false; - } - } - - public void redo() throws CannotRedoException { - if (delegate != null) { - delegate.redo(); - } - } - - public boolean canRedo() { - if (delegate != null) { - return delegate.canRedo(); - } else { - return false; - } - } - - public void die() { - if (delegate != null) { - delegate.die(); - } - } - - public boolean addEdit(UndoableEdit anEdit) { - if (delegate != null) { - return delegate.addEdit(anEdit); - } else { - return false; - } - } - - public boolean replaceEdit(UndoableEdit anEdit) { - if (delegate != null) { - return delegate.replaceEdit(anEdit); - } else { - return false; - } - } - - public boolean isSignificant() { - if (delegate != null) { - return delegate.isSignificant(); - } else { - return true; - } - } - - public String getPresentationName() { - if (delegate != null) { - return delegate.getPresentationName(); - } else { - return ""; // NOI18N - } - } - - public String getUndoPresentationName() { - if (delegate != null) { - return delegate.getUndoPresentationName(); - } else { - return ""; // NOI18N - } - } - - public String getRedoPresentationName() { - if (delegate != null) { - return delegate.getRedoPresentationName(); - } else { - return ""; // NOI18N - } - } - } - - /** Undoable edit that is put before the savepoint. Its replaceEdit() - * method will consume and wrap the edit that precedes the save. - * If the edit is added to the begining of the queue then - * the isSignificant() implementation guarantees that the edit - * will not be removed from the queue. - * When redone it marks the document as not modified. - */ - private class BeforeSaveEdit extends FilterUndoableEdit { - private long saveTime; - - BeforeSaveEdit(long saveTime) { - this.saveTime = saveTime; - } - - @Override - public boolean replaceEdit(UndoableEdit anEdit) { - if (delegate == null) { - delegate = anEdit; - - return true; // signal consumed - } - - return false; - } - - @Override - public boolean addEdit(UndoableEdit anEdit) { - if (!(anEdit instanceof BeforeModificationEdit) && !(anEdit instanceof SearchBeforeModificationEdit)) { - /* UndoRedo.addEdit() must not be done lazily - * because the edit must be "inserted" before the current one. - */ - getUndoRedo().addEdit(new BeforeModificationEdit(saveTime, anEdit)); - - return true; - } - - return false; - } - - @Override - public void redo() { - super.redo(); - - if (saveTime == lastSaveTime) { - justRevertedToNotModified = true; - } - } - - @Override - public boolean isSignificant() { - return (delegate != null); - } - } - - /** Edit that is created by wrapping the given edit. - * When undone it marks the document as not modified. - */ - private class BeforeModificationEdit extends FilterUndoableEdit { - private long saveTime; - - BeforeModificationEdit(long saveTime, UndoableEdit delegate) { - this.saveTime = saveTime; - this.delegate = delegate; - ERR.log(Level.FINEST, null, new Exception("new BeforeModificationEdit(" + saveTime +")")); // NOI18N - } - - @Override - public boolean addEdit(UndoableEdit anEdit) { - if ((delegate == null) && !(anEdit instanceof SearchBeforeModificationEdit)) { - delegate = anEdit; - - return true; - } - - return delegate.addEdit(anEdit); - } - - @Override - public void undo() { - super.undo(); - - boolean res = saveTime == lastSaveTime; - ERR.fine("Comparing saveTime and lastSaveTime: " + saveTime + "==" + lastSaveTime + " is " + res); // NOI18N - if (res) { - justRevertedToNotModified = true; - } - } - } - - /** This edit is used to search for BeforeModificationEdit in UndoRedo - * manager. This is not much nice solution, but well, there is not - * much other chances to get inside UndoRedo. - */ - private class SearchBeforeModificationEdit extends FilterUndoableEdit { - SearchBeforeModificationEdit() { - } - - @Override - public boolean replaceEdit(UndoableEdit anEdit) { - if (delegate == null) { - delegate = anEdit; - - return true; // signal consumed - } - - return false; - } - } - - /** An improved version of UndoRedo manager that locks document before - * doing any other operations. - */ - private final static class CESUndoRedoManager extends UndoGroupManager { - private CloneableEditorSupport support; - - public CESUndoRedoManager(CloneableEditorSupport c) { - this.support = c; - super.setLimit(1000); - } - - @Override - public void redo() throws javax.swing.undo.CannotRedoException { - final StyledDocument myDoc = support.getDocument(); - - if (myDoc == null) { - throw new javax.swing.undo.CannotRedoException(); // NOI18N - } - - support.justRevertedToNotModified = false; - new RenderUndo(0, myDoc); - - if (support.justRevertedToNotModified && support.isAlreadyModified()) { - support.callNotifyUnmodified(); - } - } - - @Override - public void undo() throws javax.swing.undo.CannotUndoException { - final StyledDocument myDoc = support.getDocument(); - - if (myDoc == null) { - throw new javax.swing.undo.CannotUndoException(); // NOI18N - } - - support.justRevertedToNotModified = false; - new RenderUndo(1, myDoc); - - if (support.justRevertedToNotModified && support.isAlreadyModified()) { - support.callNotifyUnmodified(); - } - } - - @Override - public boolean canRedo() { - final StyledDocument myDoc = support.getDocument(); - - return new RenderUndo(2, myDoc, 0, true).booleanResult; - } - - @Override - public boolean canUndo() { - final StyledDocument myDoc = support.getDocument(); - - return new RenderUndo(3, myDoc, 0, true).booleanResult; - } - - @Override - public int getLimit() { - final StyledDocument myDoc = support.getDocument(); - - return new RenderUndo(4, myDoc).intResult; - } - - @Override - public void discardAllEdits() { - final StyledDocument myDoc = support.getDocument(); - new RenderUndo(5, myDoc); - // Insert before-save undo event to enable unmodifying undo - undoableEditHappened(new UndoableEditEvent(support, support.new BeforeSaveEdit(support.lastSaveTime))); - } - - @Override - public void setLimit(int l) { - final StyledDocument myDoc = support.getDocument(); - new RenderUndo(6, myDoc, l); - } - - @Override - public boolean canUndoOrRedo() { - final StyledDocument myDoc = support.getDocument(); - - return new RenderUndo(7, myDoc, 0, true).booleanResult; - } - - @Override - public java.lang.String getUndoOrRedoPresentationName() { - if (support.isDocumentReady()) { - final StyledDocument myDoc = support.getDocument(); - return new RenderUndo(8, myDoc, 0, true).stringResult; - } else { - return ""; - } - } - - @Override - public java.lang.String getRedoPresentationName() { - if (support.isDocumentReady()) { - final StyledDocument myDoc = support.getDocument(); - return new RenderUndo(9, myDoc, 0, true).stringResult; - } else { - return ""; - } - } - - @Override - public java.lang.String getUndoPresentationName() { - if (support.isDocumentReady()) { - final StyledDocument myDoc = support.getDocument(); - return new RenderUndo(10, myDoc, 0, true).stringResult; - } else { - return ""; - } - } - - @Override - public void undoOrRedo() throws javax.swing.undo.CannotUndoException, javax.swing.undo.CannotRedoException { - final StyledDocument myDoc = support.getDocument(); - - if (myDoc == null) { - throw new javax.swing.undo.CannotUndoException(); // NOI18N - } - - support.justRevertedToNotModified = false; - new RenderUndo(11, myDoc); - - if (support.justRevertedToNotModified && support.isAlreadyModified()) { - support.callNotifyUnmodified(); - } - } - - private final class RenderUndo implements Runnable { - private final int type; - public boolean booleanResult; - public int intResult; - public String stringResult; - private final boolean readonly; - - public RenderUndo(int type, StyledDocument doc) { - this(type, doc, 0); - } - - public RenderUndo(int type, StyledDocument doc, int intValue) { - this(type, doc, intValue, false); - } - - public RenderUndo(int type, StyledDocument doc, int intValue, boolean readonly) { - this.type = type; - this.intResult = intValue; - this.readonly = readonly; - - if (!readonly && (doc instanceof NbDocument.WriteLockable)) { - ((NbDocument.WriteLockable) doc).runAtomic(this); - } else { - if (readonly && doc != null) { - doc.render(this); - } else { - // if the document is not one of "NetBeans ready" - // that supports locking we do not have many - // chances to do something. Maybe check for AbstractDocument - // and call writeLock using reflection, but better than - // that, let's leave this simple for now and wait for - // bug reports (if any appear) - run(); - } - } - } - - public void run() { - switch (type) { - case 0: - CESUndoRedoManager.super.redo(); - - break; - - case 1: - CESUndoRedoManager.super.undo(); - - break; - - case 2: - booleanResult = CESUndoRedoManager.super.canRedo(); - - break; - - case 3: - booleanResult = CESUndoRedoManager.super.canUndo(); - - break; - - case 4: - intResult = CESUndoRedoManager.super.getLimit(); - - break; - - case 5: - CESUndoRedoManager.super.discardAllEdits(); - - break; - - case 6: - CESUndoRedoManager.super.setLimit(intResult); - - break; - - case 7: - CESUndoRedoManager.super.canUndoOrRedo(); - - break; - - case 8: - stringResult = CESUndoRedoManager.super.getUndoOrRedoPresentationName(); - - break; - - case 9: - stringResult = CESUndoRedoManager.super.getRedoPresentationName(); - - break; - - case 10: - stringResult = CESUndoRedoManager.super.getUndoPresentationName(); - - break; - - case 11: - CESUndoRedoManager.super.undoOrRedo(); - - break; - - default: - throw new IllegalArgumentException("Unknown type: " + type); - } - } - } - } - - /** - * UndoGroupManager extends {@link UndoManager} - * and allows explicit control of what - * UndoableEdits are coalesced into compound edits, - * rather than using the rules defined by the edits themselves. - * Groups are defined using BEGIN_COMMIT_GROUP and END_COMMIT_GROUP. - * Send these to UndoableEditListener. These must always be paired. - *

- * These use cases are supported. - *

- *
    - *
  1. Default behavior is defined by {@link UndoManager}.
  2. - *
  3. UnddoableEdits issued between {@link #BEGIN_COMMIT_GROUP} - * and {@link #END_COMMIT_GROUP} are placed into a single - * {@link CompoundEdit}. - * Thus undo() and redo() treat them - * as a single undo/redo.
  4. - *
  5. BEGIN/END nest.
  6. - *
  7. Issue MARK_COMMIT_GROUP to commit accumulated - * UndoableEdits into a single CompoundEdit - * and to continue accumulating; - * an application could do this at strategic points, such as EndOfLine - * input or cursor movement.
  8. - *
- * @see UndoManager - */ - private static class UndoGroupManager extends UndoRedo.Manager { - /** signals that edits are being accumulated */ - private int buildUndoGroup; - /** accumulate edits here in undoGroup */ - private CompoundEdit undoGroup; - /** - * Signal that nested group started and that current undo group - * must be committed if edit is added. Then can avoid doing the commit - * if the nested group turns out to be empty. - */ - private int needsNestingCommit; - - /** - * Start a group of edits which will be committed as a single edit - * for purpose of undo/redo. - * Nesting semantics are that any BEGIN_COMMIT_GROUP and - * END_COMMIT_GROUP delimits a commit-group, unless the group is - * empty in which case the begin/end is ignored. - * While coalescing edits, any undo/redo/save implicitly delimits - * a commit-group. - */ - static final UndoableEdit BEGIN_COMMIT_GROUP = new CommitGroupEdit(); - /** End a group of edits. */ - static final UndoableEdit END_COMMIT_GROUP = new CommitGroupEdit(); - /** - * Any coalesced edits become a commit-group and a new commit-group - * is started. - */ - static final UndoableEdit MARK_COMMIT_GROUP = new CommitGroupEdit(); - - /** SeparateEdit tags an UndoableEdit so the - * UndoGroupManager does not coalesce it. - */ - interface SeparateEdit { - } - - private static class CommitGroupEdit extends AbstractUndoableEdit { - @Override - public boolean isSignificant() { - return false; - } - - @Override - public boolean canRedo() - { - return true; - } - - @Override - public boolean canUndo() - { - return true; - } - } - - @Override - public void undoableEditHappened(UndoableEditEvent ue) - { - if(ue.getEdit() == BEGIN_COMMIT_GROUP) { - beginUndoGroup(); - } else if(ue.getEdit() == END_COMMIT_GROUP) { - endUndoGroup(); - } else if(ue.getEdit() == MARK_COMMIT_GROUP) { - commitUndoGroup(); - } else { - super.undoableEditHappened(ue); - } - } - - /** - * Direct this UndoGroupManager to begin coalescing any - * UndoableEdits that are added into a CompoundEdit. - *

If edits are already being coalesced and some have been - * accumulated, they are flagged for commitment as an atomic group and - * a new group will be started. - * @see #addEdit - * @see #endUndoGroup - */ - private synchronized void beginUndoGroup() { - if(undoGroup != null) - needsNestingCommit++; - ERR.log(Level.FINE, "beginUndoGroup: nesting {0}", buildUndoGroup); - buildUndoGroup++; - } - - /** - * Direct this UndoGroupManager to stop coalescing edits. - * Until beginUndoGroupManager is invoked, - * any received UndoableEdits are added singly. - *

- * This has no effect if edits are not being coalesced, for example - * if beginUndoGroup has not been called. - */ - private synchronized void endUndoGroup() { - buildUndoGroup--; - ERR.log(Level.FINE, "endUndoGroup: nesting {0}", buildUndoGroup); - if(buildUndoGroup < 0) { - ERR.log(Level.INFO, null, new Exception("endUndoGroup without beginUndoGroup")); - // slam buildUndoGroup to 0 to disable nesting - buildUndoGroup = 0; - } - if(needsNestingCommit <= 0) - commitUndoGroup(); - if(--needsNestingCommit < 0) - needsNestingCommit = 0; - } - - /** - * Commit any accumulated UndoableEdits as an atomic - * undo/redo group. {@link CompoundEdit#end} - * is invoked on the CompoundEdit and it is added as a single - * UndoableEdit to this UndoManager. - *

- * If edits are currently being coalesced, a new undo group is started. - * This has no effect if edits are not being coalesced, for example - * beginUndoGroup has not been called. - */ - private synchronized void commitUndoGroup() { - if(undoGroup == null) { - return; - } - - // undoGroup is being set to null, - // needsNestingCommit has no meaning now - needsNestingCommit = 0; - - // super.addEdit may end up in this.addEdit, - // so buildUndoGroup must be false - int saveBuildUndoGroup = buildUndoGroup; - buildUndoGroup = 0; - - undoGroup.end(); - super.addEdit(undoGroup); - undoGroup = null; - - buildUndoGroup = saveBuildUndoGroup; - } - - /** Add this edit separately, not part of a group. - * @return super.addEdit - */ - private boolean commitAddEdit(UndoableEdit anEdit) { - commitUndoGroup(); - - int saveBuildUndoGroup = buildUndoGroup; - buildUndoGroup = 0; - boolean f = super.addEdit(anEdit); - //boolean f = addEdit(undoGroup); - buildUndoGroup = saveBuildUndoGroup; - return f; - } - - /** - * If there's a pending undo group that needs to be committed - * then commit it. - * If this UndoManager is coalescing edits then add - * anEdit to the accumulating CompoundEdit. - * Otherwise, add it to this UndoManager. In either case the - * edit is saved for later undo or redo. - * @return {@inheritDoc} - * @see #beginUndoGroup - * @see #endUndoGroup - */ - @Override - public synchronized boolean addEdit(UndoableEdit anEdit) { - if(!isInProgress()) - return false; - - if(needsNestingCommit > 0) { - commitUndoGroup(); - } - - if(buildUndoGroup > 0) { - if(anEdit instanceof SeparateEdit) - return commitAddEdit(anEdit); - if(undoGroup == null) - undoGroup = new CompoundEdit(); - return undoGroup.addEdit(anEdit); - } else { - return super.addEdit(anEdit); - } - } - - @Override - public synchronized void discardAllEdits() { - commitUndoGroup(); - super.discardAllEdits(); - } - - // - // TODO: limits - // - - @Override - public synchronized void undoOrRedo() { - commitUndoGroup(); - super.undoOrRedo(); - } - - @Override - public synchronized boolean canUndoOrRedo() { - if(undoGroup != null) - return true; - return super.canUndoOrRedo(); - } - - @Override - public synchronized void undo() { - commitUndoGroup(); - super.undo(); - } - - @Override - public synchronized boolean canUndo() { - if(undoGroup != null) - return true; - return super.canUndo(); - } - - @Override - public synchronized void redo() { - if(undoGroup != null) - throw new CannotRedoException(); - super.redo(); - } - - @Override - public synchronized boolean canRedo() { - if(undoGroup != null) - return false; - return super.canRedo(); - } - - @Override - public synchronized void end() { - commitUndoGroup(); - super.end(); - } - - @Override - public synchronized String getUndoOrRedoPresentationName() { - if(undoGroup != null) - return undoGroup.getUndoPresentationName(); - return super.getUndoOrRedoPresentationName(); - } - - @Override - public synchronized String getUndoPresentationName() { - if(undoGroup != null) - return undoGroup.getUndoPresentationName(); - return super.getUndoPresentationName(); - } - - @Override - public synchronized String getRedoPresentationName() { - if(undoGroup != null) - return undoGroup.getRedoPresentationName(); - return super.getRedoPresentationName(); - } - - @Override - public boolean isSignificant() { - if(undoGroup != null && undoGroup.isSignificant()) { - return true; - } - return super.isSignificant(); - } - - @Override - public synchronized void die() { - commitUndoGroup(); - super.die(); - } - - @Override - public String getPresentationName() { - if(undoGroup != null) - return undoGroup.getPresentationName(); - return super.getPresentationName(); - } - - // The protected methods are only accessed from - // synchronized methods that do commitUndoGroup - // so they do not need to be overridden in this class - } - /** Special runtime exception that holds the original I/O failure. */ static final class DelegateIOExc extends IllegalStateException { @@ -3791,4 +2954,82 @@ } } + private final class DocFilter extends DocumentFilter { + + final DocumentFilter origFilter; + + DocFilter(DocumentFilter origFilter) { + this.origFilter = origFilter; + } + + @Override + public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException { + boolean origModified = checkModificationAllowed(offset); + boolean success = false; + try { + if (origFilter != null) { + origFilter.insertString(fb, offset, string, attr); + } else { + super.insertString(fb, offset, string, attr); + } + success = true; + } finally { + if (!success) { + if (!origModified) { + callNotifyUnmodified(); + } + } + } + } + + @Override + public void remove(FilterBypass fb, int offset, int length) throws BadLocationException { + boolean origModified = checkModificationAllowed(offset); + boolean success = false; + try { + if (origFilter != null) { + origFilter.remove(fb, offset, length); + } else { + super.remove(fb, offset, length); + } + success = true; + } finally { + if (!success) { + if (!origModified) { + callNotifyUnmodified(); + } + } + } + } + + @Override + public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { + boolean origModified = checkModificationAllowed(offset); + boolean success = false; + try { + if (origFilter != null) { + origFilter.replace(fb, offset, length, text, attrs); + } else { + super.replace(fb, offset, length, text, attrs); + } + success = true; + } finally { + if (!success) { + if (!origModified) { + callNotifyUnmodified(); + } + } + } + } + + private boolean checkModificationAllowed(int offset) throws BadLocationException { + boolean alreadyModified = isAlreadyModified(); + if (!callNotifyModified()) { + throw new BadLocationException("Modification not allowed", offset); // NOI18N + } + return alreadyModified; + } + + } + } diff --git a/openide.text/src/org/openide/text/FilterDocument.java b/openide.text/src/org/openide/text/FilterDocument.java --- a/openide.text/src/org/openide/text/FilterDocument.java +++ b/openide.text/src/org/openide/text/FilterDocument.java @@ -99,12 +99,19 @@ /* Gets document property by key */ public Object getProperty(Object key) { + if (key == DocumentFilter.class && original instanceof AbstractDocument) { + return ((AbstractDocument)original).getDocumentFilter(); + } return original.getProperty(key); } /* Puts new property of document */ public void putProperty(Object key, Object value) { - original.putProperty(key, value); + if (key == DocumentFilter.class && original instanceof AbstractDocument) { + ((AbstractDocument)original).setDocumentFilter((DocumentFilter)value); + } else { + original.putProperty(key, value); + } } /* Removes portion of a document */ diff --git a/openide.text/src/org/openide/text/UndoGroupManager.java b/openide.text/src/org/openide/text/UndoGroupManager.java new file mode 100644 --- /dev/null +++ b/openide.text/src/org/openide/text/UndoGroupManager.java @@ -0,0 +1,373 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun + * Microsystems, Inc. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ + +package org.openide.text; + +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.event.UndoableEditEvent; +import javax.swing.undo.*; +import org.openide.awt.UndoRedo; + + +/** + * UndoGroupManager extends {@link UndoManager} + * and allows explicit control of what + * UndoableEdits are coalesced into compound edits, + * rather than using the rules defined by the edits themselves. + * Groups are defined using BEGIN_COMMIT_GROUP and END_COMMIT_GROUP. + * Send these to UndoableEditListener. These must always be paired. + *

+ * These use cases are supported. + *

+ *
    + *
  1. Default behavior is defined by {@link UndoManager}.
  2. + *
  3. UnddoableEdits issued between {@link #BEGIN_COMMIT_GROUP} + * and {@link #END_COMMIT_GROUP} are placed into a single + * {@link CompoundEdit}. + * Thus undo() and redo() treat them + * as a single undo/redo.
  4. + *
  5. BEGIN/END nest.
  6. + *
  7. Issue MARK_COMMIT_GROUP to commit accumulated + * UndoableEdits into a single CompoundEdit + * and to continue accumulating; + * an application could do this at strategic points, such as EndOfLine + * input or cursor movement.
  8. + *
+ * @see UndoManager + * @author Ernie Rael + */ +class UndoGroupManager extends UndoRedo.Manager { + + // -J-Dorg.openide.text.UndoGroupManager.level=FINE + private static final Logger LOG = Logger.getLogger(UndoGroupManager.class.getName()); + + /** signals that edits are being accumulated */ + private int buildUndoGroup; + + /** accumulate edits here in undoGroup */ + private CompoundEdit undoGroup; + + /** + * Signal that nested group started and that current undo group + * must be committed if edit is added. Then can avoid doing the commit + * if the nested group turns out to be empty. + */ + private int needsNestingCommit; + + /** + * Start a group of edits which will be committed as a single edit + * for purpose of undo/redo. + * Nesting semantics are that any BEGIN_COMMIT_GROUP and + * END_COMMIT_GROUP delimits a commit-group, unless the group is + * empty in which case the begin/end is ignored. + * While coalescing edits, any undo/redo/save implicitly delimits + * a commit-group. + */ + static final UndoableEdit BEGIN_COMMIT_GROUP = new CommitGroupEdit(); + + /** End a group of edits. */ + static final UndoableEdit END_COMMIT_GROUP = new CommitGroupEdit(); + + /** + * Any coalesced edits become a commit-group and a new commit-group + * is started. + */ + static final UndoableEdit MARK_COMMIT_GROUP = new CommitGroupEdit(); + + private static class CommitGroupEdit extends AbstractUndoableEdit { + @Override + public boolean isSignificant() { + return false; + } + + @Override + public boolean canRedo() + { + return true; + } + + @Override + public boolean canUndo() + { + return true; + } + } + + @Override + public void undoableEditHappened(UndoableEditEvent ue) + { + if(ue.getEdit() == BEGIN_COMMIT_GROUP) { + beginUndoGroup(); + } else if(ue.getEdit() == END_COMMIT_GROUP) { + endUndoGroup(); + } else if(ue.getEdit() == MARK_COMMIT_GROUP) { + commitUndoGroup(); + } else { + super.undoableEditHappened(ue); + } + } + + /** + * Direct this UndoGroupManager to begin coalescing any + * UndoableEdits that are added into a CompoundEdit. + *

If edits are already being coalesced and some have been + * accumulated, they are flagged for commitment as an atomic group and + * a new group will be started. + * @see #addEdit + * @see #endUndoGroup + */ + private synchronized void beginUndoGroup() { + if(undoGroup != null) + needsNestingCommit++; + LOG.log(Level.FINE, "beginUndoGroup: nesting {0}", buildUndoGroup); + buildUndoGroup++; + } + + /** + * Direct this UndoGroupManager to stop coalescing edits. + * Until beginUndoGroupManager is invoked, + * any received UndoableEdits are added singly. + *

+ * This has no effect if edits are not being coalesced, for example + * if beginUndoGroup has not been called. + */ + private synchronized void endUndoGroup() { + buildUndoGroup--; + LOG.log(Level.FINE, "endUndoGroup: nesting {0}", buildUndoGroup); + if(buildUndoGroup < 0) { + LOG.log(Level.INFO, null, new Exception("endUndoGroup without beginUndoGroup")); + // slam buildUndoGroup to 0 to disable nesting + buildUndoGroup = 0; + } + if(needsNestingCommit <= 0) + commitUndoGroup(); + if(--needsNestingCommit < 0) + needsNestingCommit = 0; + } + + /** + * Commit any accumulated UndoableEdits as an atomic + * undo/redo group. {@link CompoundEdit#end} + * is invoked on the CompoundEdit and it is added as a single + * UndoableEdit to this UndoManager. + *

+ * If edits are currently being coalesced, a new undo group is started. + * This has no effect if edits are not being coalesced, for example + * beginUndoGroup has not been called. + */ + private synchronized void commitUndoGroup() { + if(undoGroup == null) { + return; + } + + // undoGroup is being set to null, + // needsNestingCommit has no meaning now + needsNestingCommit = 0; + + // super.addEdit may end up in this.addEdit, + // so buildUndoGroup must be false + int saveBuildUndoGroup = buildUndoGroup; + buildUndoGroup = 0; + + undoGroup.end(); + super.addEdit(undoGroup); + undoGroup = null; + + buildUndoGroup = saveBuildUndoGroup; + } + + /** Add this edit separately, not part of a group. + * @return super.addEdit + */ + private boolean commitAddEdit(UndoableEdit anEdit) { + commitUndoGroup(); + + int saveBuildUndoGroup = buildUndoGroup; + buildUndoGroup = 0; + boolean f = super.addEdit(anEdit); + //boolean f = addEdit(undoGroup); + buildUndoGroup = saveBuildUndoGroup; + return f; + } + + /** + * If there's a pending undo group that needs to be committed + * then commit it. + * If this UndoManager is coalescing edits then add + * anEdit to the accumulating CompoundEdit. + * Otherwise, add it to this UndoManager. In either case the + * edit is saved for later undo or redo. + * @return {@inheritDoc} + * @see #beginUndoGroup + * @see #endUndoGroup + */ + @Override + public synchronized boolean addEdit(UndoableEdit anEdit) { + if(!isInProgress()) + return false; + + if(needsNestingCommit > 0) { + commitUndoGroup(); + } + + if(buildUndoGroup > 0) { + if(anEdit instanceof WrapUndoEdit) + return commitAddEdit(anEdit); + if(undoGroup == null) + undoGroup = new CompoundEdit(); + return undoGroup.addEdit(anEdit); + } else { + return super.addEdit(anEdit); + } + } + + @Override + public synchronized void discardAllEdits() { + commitUndoGroup(); + super.discardAllEdits(); + } + + // + // TODO: limits + // + + @Override + public synchronized void undoOrRedo() { + commitUndoGroup(); + super.undoOrRedo(); + } + + @Override + public synchronized boolean canUndoOrRedo() { + if(undoGroup != null) + return true; + return super.canUndoOrRedo(); + } + + @Override + public synchronized void undo() { + commitUndoGroup(); + super.undo(); + } + + @Override + public synchronized boolean canUndo() { + if(undoGroup != null) + return true; + return super.canUndo(); + } + + @Override + public synchronized void redo() { + if(undoGroup != null) + throw new CannotRedoException(); + super.redo(); + } + + @Override + public synchronized boolean canRedo() { + if(undoGroup != null) + return false; + return super.canRedo(); + } + + @Override + public synchronized void end() { + commitUndoGroup(); + super.end(); + } + + @Override + public synchronized String getUndoOrRedoPresentationName() { + if(undoGroup != null) + return undoGroup.getUndoPresentationName(); + return super.getUndoOrRedoPresentationName(); + } + + @Override + public synchronized String getUndoPresentationName() { + if(undoGroup != null) + return undoGroup.getUndoPresentationName(); + return super.getUndoPresentationName(); + } + + @Override + public synchronized String getRedoPresentationName() { + if(undoGroup != null) + return undoGroup.getRedoPresentationName(); + return super.getRedoPresentationName(); + } + + @Override + public boolean isSignificant() { + if(undoGroup != null && undoGroup.isSignificant()) { + return true; + } + return super.isSignificant(); + } + + @Override + public synchronized void die() { + commitUndoGroup(); + super.die(); + } + + @Override + public String getPresentationName() { + if(undoGroup != null) + return undoGroup.getPresentationName(); + return super.getPresentationName(); + } + + @Override + public final WrapUndoEdit lastEdit() { // make lastEdit() accessible + return (WrapUndoEdit) super.lastEdit(); + } + + // The protected methods are only accessed from + // synchronized methods that do commitUndoGroup + // so they do not need to be overridden in this class + +} diff --git a/openide.text/src/org/openide/text/UndoRedoManager.java b/openide.text/src/org/openide/text/UndoRedoManager.java new file mode 100644 --- /dev/null +++ b/openide.text/src/org/openide/text/UndoRedoManager.java @@ -0,0 +1,527 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun + * Microsystems, Inc. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ + +package org.openide.text; + +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.text.StyledDocument; +import javax.swing.undo.*; + + +/** + * An improved version of UndoRedo manager that locks document before + * doing any other operations. + *
+ * It supports grouping of undoable edits by extending UndoGroupManager. + *
+ * It supports save actions that produce a compound undoable edit. + *
+ * + *

+ * Following requirements should be met: + *

    + *
  • When saving document extra save actions are performed producing compound saveActionsEdit..
  • + *
  • When undoing just performed save the saveActionsEdit should be undone at once with the last + * performed edit.
  • + *
  • When save of the document was just performed a next edit must not be merged with last edit + * so that the savepoint can be retained.
  • + *
  • + *
+ *

+ * + * @author Miloslav Metelka + * @author Jaroslav Tulach + */ +final class UndoRedoManager extends UndoGroupManager { + + // -J-Dorg.openide.text.UndoRedoManager.level=FINE + private static final Logger LOG = Logger.getLogger(UndoRedoManager.class.getName()); + + /** + * Marker edit for the state when undo manager is right at the savepoint. + *
+ * Next performed edit will set afterSaveEdit field. + */ + static final UndoableEdit SAVEPOINT = new CompoundEdit(); + + CloneableEditorSupport support; + + /** + * Undo edit that is right after/before "save point" in undo manager's queue. + *
+ * Subsequent addEdit(), undo() and redo() operations will modify + * the field to point to neighbor edit of the save point. + *
+ * The field may be set to SAVEPOINT special value in case the undo manager + * is set right to the savepoint. + */ + UndoableEdit savepointEdit; + + /** + * Whether undo manager's undo()/redo() operations currently operate before/after + * the savepoint (when right at savepoint the value is undefined). + */ + boolean beforeSavepoint; + + /** + * Edit that was added by last performed operation (undo clears this field). + */ + UndoableEdit lastAddedOrRedoneEdit; + + /** + * Undoable edit created as result of running save actions by using + * "beforeSaveRunnable" document property (See CloneableEditorSupport). + *
+ * If saving occurs right at UM.edits end (i.e. either UM.addEdit() was just performed + * or lastAddedEdit.redo() was performed) then the save actions edit + * is merged with the last added edit. + *
+ * If UM.edits is empty (it could possibly happen when document is not saved + * and discardAllEdits() was called for some reason) then the save actions edit + * is not added to UM.edits (just edit.die() is called). + *
+ * When the save actions edit is done in any other situation then it must be + *
    + *
  • undone when the UM is at savepoint and UM.undo() or UM.redo() is done.
  • + *
  • redone when edit right before savepoint is redone.
  • + *
  • redone when edit right after savepoint is undone.
  • + *
+ */ + private CompoundEdit saveActionsEdit; + + /** + * Set to true when the CES gave control to save actions that may produce + * undoable edits which need to be coallesced into a special compoound edit. + */ + private boolean performingSaveActions; + + /** + * Flag to check whether support.notifyUnmodified() should be called + * - it's necessary to do it outside of atomicLock acquired by DocLockedRun. + */ + private boolean callNotifyUnmodified; + + + public UndoRedoManager(CloneableEditorSupport support) { + this.support = support; + super.setLimit(1000); + } + + void setPerformingSaveActions(boolean performingSaveActions) { + if (performingSaveActions != this.performingSaveActions) { + this.performingSaveActions = performingSaveActions; + if (performingSaveActions) { + clearSaveActionsEdit(); + saveActionsEdit = new CompoundEdit(); + checkLogOp(" NEW-saveActionsEdit", saveActionsEdit); // NOI18N + } else { // Stop performing save actions + saveActionsEdit.end(); + checkLogOp(" COMPLETED-saveActionsEdit", saveActionsEdit); // NOI18N + UndoableEdit lastEdit = lastEdit(); + checkLogOp(" lastEdit", lastEdit); // NOI18N + if (lastAddedOrRedoneEdit == lastEdit) { // At UM.edits end + if (lastEdit != null) { // Concatenate with last one + WrapUndoEdit lastWrapEdit = (WrapUndoEdit) lastEdit; + CompoundEdit compoundEdit = new CompoundEdit(); + compoundEdit.addEdit(lastWrapEdit.delegate()); + compoundEdit.addEdit(saveActionsEdit); + compoundEdit.end(); + lastWrapEdit.setDelegate(compoundEdit); + checkLogOp(" compoundEdit", compoundEdit); // NOI18N + + } else { // No edits present in UM.edits + // This state might happen when e.g. discardAllEdits() was called + // but the document was not at savepoint (otherwise the document + // would be saved so no save actions would be performed). + // To not make the save actions an extra edit in the UM.edits + // the save actions are not added to the queue. + saveActionsEdit.die(); + checkLogOp(" null-lastEdit->saveActionsEdit.die", saveActionsEdit); // NOI18N + } + saveActionsEdit = null; // Save actions serviced + + } else { // Save occurred somewhere inside existing UM.edits + // saveActionsEdit contains changes done before save operation. + // To enable redo of existing changes in UM.edits + // this extra edit must be undone when doing undo or redo from savepoint + // and redone when doing undo or redo to savepoint. + } + } + } + } + + void markSavepoint() { + savepointEdit = SAVEPOINT; + } + + boolean isAtSavepoint() { + return (savepointEdit == SAVEPOINT); + } + + private void markSavepointAndUnmodified() { + savepointEdit = SAVEPOINT; + callNotifyUnmodified = true; + } + + private void checkCallNotifyUnmodified() { + if (callNotifyUnmodified) { + callNotifyUnmodified = false; + if (support.isAlreadyModified()) { + support.callNotifyUnmodified(); + } + } + } + + void beforeUndoCheck(WrapUndoEdit edit) { + if (isAtSavepoint()) { // Undoing edit right before savepoint. + checkLogOp("beforeUndoCheck-atSavepoint", edit); // NOI18N + if (saveActionsEdit != null) { + checkLogOp(" saveActionsEdit.undo()", saveActionsEdit); // NOI18N + saveActionsEdit.undo(); + } + } + } + + void afterUndoCheck(WrapUndoEdit edit) { + if (isAtSavepoint()) { // Undoing edit right before savepoint. + checkLogOp("afterUndoCheck-atSavepoint", edit); // NOI18N + // saveActionsEdit already processed by checkSavepointBeforeUndo() + beforeSavepoint = true; + savepointEdit = edit; + + } else if (savepointEdit == edit) { // Undone to savepoint + if (saveActionsEdit != null) { + checkLogOp(" saveActionsEdit.redo()", saveActionsEdit); // NOI18N + saveActionsEdit.redo(); + } + checkLogOp("afterUndoCheck-becomesSavepoint-markUnmodified", edit); // NOI18N + assert (!beforeSavepoint) : "Expected to be behind savepoint"; // NOI18N + markSavepointAndUnmodified(); + } + lastAddedOrRedoneEdit = null; + } + + void beforeRedoCheck(WrapUndoEdit edit) { + if (isAtSavepoint()) { // Redoing edit right before savepoint. + checkLogOp("beforeRedoCheck-atSavepoint", edit); // NOI18N + if (saveActionsEdit != null) { + checkLogOp(" saveActionsEdit.undo()", saveActionsEdit); // NOI18N + saveActionsEdit.undo(); + } + } + } + + void afterRedoCheck(WrapUndoEdit edit) { + if (isAtSavepoint()) { // Redoing edit right before savepoint. + checkLogOp("afterRedoCheck-atSavepoint", edit); // NOI18N + // saveActionsEdit already processed by checkSavepointBeforeUndo() + beforeSavepoint = false; + savepointEdit = edit; + + } else if (savepointEdit == edit) { // Redone to savepoint + if (saveActionsEdit != null) { + checkLogOp(" saveActionsEdit.redo()", saveActionsEdit); // NOI18N + saveActionsEdit.redo(); + } + checkLogOp("afterRedoCheck-becomesSavepoint", edit); // NOI18N + assert (beforeSavepoint) : "Expected to be before savepoint"; // NOI18N + markSavepointAndUnmodified(); + } + lastAddedOrRedoneEdit = edit; + } + + void checkReplaceSavepointEdit(WrapUndoEdit origEdit, WrapUndoEdit newEdit) { + if (savepointEdit == origEdit) { + checkLogOp("checkReplaceSavepointEdit-replacedSavepointEdit", origEdit); // NOI18N + savepointEdit = newEdit; + } + } + + void notifyWrapEditDie(UndoableEdit edit) { + if (edit == savepointEdit) { // Savepoint neighbour died => no longer a savepoint + checkLogOp("notifyWrapEditDie-savepoint-die", edit); // NOI18N + savepointEdit = null; + clearSaveActionsEdit(); + } + } + + @Override + public synchronized boolean addEdit(UndoableEdit edit) { + // This should already be called under document's lock so DocLockedRun not necessary + assert (edit != null) : "Cannot add null edit"; // NOI18N + if (performingSaveActions) { + checkLogOp("addEdit-performingSaveActions", edit); // NOI18N + boolean added = saveActionsEdit.addEdit(edit); + if (!added) { + throw new IllegalStateException("Cannot add to saveActionsEdit"); // NOI18N + } + return added; + } + WrapUndoEdit wrapEdit = new WrapUndoEdit(this, edit); // Wrap the edit + boolean added = super.addEdit(wrapEdit); + if (isAtSavepoint()) { + checkLogOp("addEdit-atSavepoint", wrapEdit); // NOI18N + beforeSavepoint = false; + savepointEdit = wrapEdit; + } else { + checkLogOp("addEdit", wrapEdit); // NOI18N + } + lastAddedOrRedoneEdit = wrapEdit; + return added; + } + + // replaceEdit() not overriden - it should return false + + @Override + public void redo() throws javax.swing.undo.CannotRedoException { + final StyledDocument doc = support.getDocument(); + if (doc == null) { + throw new javax.swing.undo.CannotRedoException(); // NOI18N + } + new DocLockedRun(0, doc); + checkCallNotifyUnmodified(); + } + + @Override + public void undo() throws javax.swing.undo.CannotUndoException { + final StyledDocument doc = support.getDocument(); + if (doc == null) { + throw new javax.swing.undo.CannotUndoException(); // NOI18N + } + new DocLockedRun(1, doc); + checkCallNotifyUnmodified(); + } + + @Override + public boolean canRedo() { + return new DocLockedRun(2, support.getDocument(), 0, true).booleanResult; + } + + @Override + public boolean canUndo() { + return new DocLockedRun(3, support.getDocument(), 0, true).booleanResult; + } + + @Override + public int getLimit() { + return new DocLockedRun(4, support.getDocument()).intResult; + } + + @Override + public void discardAllEdits() { + new DocLockedRun(5, support.getDocument()); + } + + private void clearSaveActionsEdit() { + if (saveActionsEdit != null) { + checkLogOp(" saveActionsEdit-die", saveActionsEdit); // NOI18N + saveActionsEdit.die(); + saveActionsEdit = null; + } + } + + @Override + public void setLimit(int l) { + new DocLockedRun(6, support.getDocument(), l); + } + + @Override + public boolean canUndoOrRedo() { + return new DocLockedRun(7, support.getDocument(), 0, true).booleanResult; + } + + @Override + public java.lang.String getUndoOrRedoPresentationName() { + if (support.isDocumentReady()) { + return new DocLockedRun(8, support.getDocument(), 0, true).stringResult; + } else { + return ""; + } + } + + @Override + public java.lang.String getRedoPresentationName() { + if (support.isDocumentReady()) { + return new DocLockedRun(9, support.getDocument(), 0, true).stringResult; + } else { + return ""; + } + } + + @Override + public java.lang.String getUndoPresentationName() { + if (support.isDocumentReady()) { + return new DocLockedRun(10, support.getDocument(), 0, true).stringResult; + } else { + return ""; + } + } + + @Override + public void undoOrRedo() throws javax.swing.undo.CannotUndoException, javax.swing.undo.CannotRedoException { + super.undoOrRedo(); + } + + static String editToString(UndoableEdit edit) { + if (edit instanceof WrapUndoEdit) { + return toStringTerse(edit) + "->" + toStringTerse(((WrapUndoEdit)edit).delegate()); // NOI18N + } else { + return toStringTerse(edit); + } + } + + static String toStringTerse(Object o) { + if (o != null) { + String clsName = o.getClass().getName(); + return clsName.substring(clsName.lastIndexOf('.') + 1) + "@" + System.identityHashCode(o); // NOI18N + } else { + return "null"; // NOI18N + } + } + + static void checkLogOp(String op, UndoableEdit edit) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine(op + ": " + editToString(edit) + '\n'); + } + } + + private final class DocLockedRun implements Runnable { + + private final int type; + + boolean booleanResult; + + int intResult; + + String stringResult; + + public DocLockedRun(int type, StyledDocument doc) { + this(type, doc, 0); + } + + public DocLockedRun(int type, StyledDocument doc, int intValue) { + this(type, doc, intValue, false); + } + + public DocLockedRun(int type, StyledDocument doc, int intValue, boolean readLock) { + this.type = type; + this.intResult = intValue; + + if (!readLock && (doc instanceof NbDocument.WriteLockable)) { + ((NbDocument.WriteLockable) doc).runAtomic(this); + } else { + if (readLock && doc != null) { + doc.render(this); + } else { + // if the document is not one of "NetBeans ready" + // that supports locking we do not have many + // chances to do something. Maybe check for AbstractDocument + // and call writeLock using reflection, but better than + // that, let's leave this simple for now and wait for + // bug reports (if any appear) + run(); + } + } + } + + public void run() { + switch (type) { + case 0: + UndoRedoManager.super.redo(); + break; + + case 1: + UndoRedoManager.super.undo(); + break; + + case 2: + booleanResult = UndoRedoManager.super.canRedo(); + break; + + case 3: + booleanResult = UndoRedoManager.super.canUndo(); + break; + + case 4: + intResult = UndoRedoManager.super.getLimit(); + break; + + case 5: + UndoRedoManager.super.discardAllEdits(); + clearSaveActionsEdit(); + lastAddedOrRedoneEdit = null; + break; + + case 6: + UndoRedoManager.super.setLimit(intResult); + break; + + case 7: + UndoRedoManager.super.canUndoOrRedo(); + break; + + case 8: + stringResult = UndoRedoManager.super.getUndoOrRedoPresentationName(); + break; + + case 9: + stringResult = UndoRedoManager.super.getRedoPresentationName(); + break; + + case 10: + stringResult = UndoRedoManager.super.getUndoPresentationName(); + break; + + case 11: + UndoRedoManager.super.undoOrRedo(); + break; + + default: + throw new IllegalArgumentException("Unknown type: " + type); + } + } + } + +} diff --git a/openide.text/src/org/openide/text/WrapUndoEdit.java b/openide.text/src/org/openide/text/WrapUndoEdit.java new file mode 100644 --- /dev/null +++ b/openide.text/src/org/openide/text/WrapUndoEdit.java @@ -0,0 +1,171 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2011 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development and + * Distribution License("CDDL") (collectively, the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy of + * the License at http://www.netbeans.org/cddl-gplv2.html or + * nbbuild/licenses/CDDL-GPL-2-CP. See the License for the specific language + * governing permissions and limitations under the License. When distributing + * the software, include this License Header Notice in each file and include + * the License file at nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided by + * Oracle in the GPL Version 2 section of the License file that accompanied + * this code. If applicable, add the following below the License Header, with + * the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you do not indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to its + * licensees as provided above. However, if you add GPL Version 2 code and + * therefore, elected the GPL Version 2 license, then the option applies only + * if the new code is made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2011 Sun Microsystems, Inc. + */ +package org.openide.text; + +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import javax.swing.undo.UndoableEdit; + +/** + * Undoable edit that wraps each undoable edit added to CloneableEditorSupport's undo manager. + *
+ * It is needed to + *
    + *
  • Prohibit addEdit() operation on last edit when the undo manager is at savepoint.
  • + *
  • Prohibit replaceEdit() operation on last edit when the undo manager is at savepoint.
  • + *
  • Identify non-significant edits that are otherwise not reported by editToBeUndone() + * and editToBeRedone().
  • + *
+ * + * @author Miloslav Metelka + * @since + */ +final class WrapUndoEdit implements UndoableEdit { + + /** + * Support for which this edit is created. + */ + final UndoRedoManager undoRedoManager; // 8=Object + 4 = 12 bytes + + /** + * Real undoable edit passed to undoableEditHappened(). + */ + private UndoableEdit delegate; // 12 + 4 = 16 bytes + + WrapUndoEdit(UndoRedoManager undoRedoManager, UndoableEdit delegate) { + assert (delegate != null) : "Delegate is null"; // NOI18N + this.undoRedoManager = undoRedoManager; + this.delegate = delegate; + } + + UndoableEdit delegate() { + return delegate; + } + + void setDelegate(UndoableEdit delegate) { + this.delegate = delegate; + } + + @Override + public void undo() throws CannotUndoException { + UndoRedoManager.checkLogOp("WrapUndoEdit.undo", this); + undoRedoManager.beforeUndoCheck(this); + delegate.undo(); + // This will only happen if delegate.undo() does not throw CannotUndoException + undoRedoManager.afterUndoCheck(this); + } + + @Override + public boolean canUndo() { + return delegate.canUndo(); + } + + @Override + public void redo() throws CannotRedoException { + UndoRedoManager.checkLogOp("WrapUndoEdit.redo", this); + undoRedoManager.beforeRedoCheck(this); + delegate.redo(); + // This will only happen if delegate.redo() does not throw CannotRedoException + undoRedoManager.afterRedoCheck(this); + } + + @Override + public boolean canRedo() { + return delegate.canRedo(); + } + + @Override + public void die() { + UndoRedoManager.checkLogOp("WrapUndoEdit.die", this); + delegate.die(); + undoRedoManager.notifyWrapEditDie(this); + } + + @Override + public boolean addEdit(UndoableEdit anEdit) { + if (undoRedoManager.isAtSavepoint()) { + // Prohibit adding to existing edit at savepoint since undo to savepoint could no longer be done + // when merging with another edit. + // Since addEdit() will fail the anEdit.replaceEdit() will be attempted; but it will not succeed too + // (see impl) so anEdit will be added as a fresh edit to end of UM.edits. + // However undoRedoManager.savepointEdit must be set by undoRedoManager.addEdit() since + // UM.edits may be empty in which case WUE.addEdit() is not called. + return false; + } + // anEdit is already wrapped for possible regular addition + WrapUndoEdit wrapEdit = (WrapUndoEdit) anEdit; + boolean added = delegate.addEdit(wrapEdit.delegate); + return added; + } + + @Override + public boolean replaceEdit(UndoableEdit anEdit) { + if (undoRedoManager.isAtSavepoint()) { // Prohibit replacing at savepoint + return false; + } + WrapUndoEdit wrapEdit = (WrapUndoEdit) anEdit; + boolean replaced = delegate.replaceEdit(wrapEdit.delegate); + UndoRedoManager.checkLogOp("WrapUndoEdit.replaceEdit=" + replaced, anEdit); + if (replaced) { + undoRedoManager.checkReplaceSavepointEdit(wrapEdit, this); + } + return replaced; + } + + @Override + public boolean isSignificant() { + return delegate.isSignificant(); + } + + @Override + public String getPresentationName() { + return delegate.getPresentationName(); + } + + @Override + public String getUndoPresentationName() { + return delegate.getUndoPresentationName(); + } + + @Override + public String getRedoPresentationName() { + return delegate.getRedoPresentationName(); + } + +} + diff --git a/openide.text/test/unit/src/org/openide/text/CloneableEditorSupportRedirectorTest.java b/openide.text/test/unit/src/org/openide/text/CloneableEditorSupportRedirectorTest.java --- a/openide.text/test/unit/src/org/openide/text/CloneableEditorSupportRedirectorTest.java +++ b/openide.text/test/unit/src/org/openide/text/CloneableEditorSupportRedirectorTest.java @@ -38,6 +38,7 @@ import java.lang.ref.WeakReference; import java.util.Arrays; import javax.swing.SwingUtilities; +import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.EditorKit; import org.netbeans.junit.MockServices; @@ -157,12 +158,12 @@ assertFalse ("Nothing to undo", red.master.getUndoRedo ().canUndo ()); // this should not be allowed - doc.insertString (0, "Kuk", null); - - String modifiedForAWhile = doc.getText (0, 3); - //assertEquals ("For a while the test really starts with Kuk", "Kuk", doc.getText (0, 3)); - - assertFalse ("The document cannot be modified", red.master.getUndoRedo ().canUndo ()); + try { + doc.insertString (0, "Kuk", null); + fail("Modification should not proceed"); + } catch (BadLocationException ex) { + // Expected + } String s = doc.getText (0, doc.getLength ()); assertEquals ("The document is now the same as at the begining", content, s); diff --git a/openide.text/test/unit/src/org/openide/text/CloneableEditorSupportTest.java b/openide.text/test/unit/src/org/openide/text/CloneableEditorSupportTest.java --- a/openide.text/test/unit/src/org/openide/text/CloneableEditorSupportTest.java +++ b/openide.text/test/unit/src/org/openide/text/CloneableEditorSupportTest.java @@ -56,6 +56,7 @@ import java.util.Date; import javax.swing.JEditorPane; import javax.swing.SwingUtilities; +import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.EditorKit; import junit.framework.AssertionFailedError; @@ -265,12 +266,12 @@ assertFalse ("Nothing to undo", support.getUndoRedo ().canUndo ()); // this should not be allowed - doc.insertString (0, "Kuk", null); - - String modifiedForAWhile = doc.getText (0, 3); - //assertEquals ("For a while the test really starts with Kuk", "Kuk", doc.getText (0, 3)); - - assertFalse ("The document cannot be modified", support.getUndoRedo ().canUndo ()); + try { + doc.insertString (0, "Kuk", null); + fail("Modification should not proceed"); + } catch (BadLocationException ex) { + // Expected + } String s = doc.getText (0, doc.getLength ()); assertEquals ("The document is now the same as at the begining", content, s);