diff --git a/editor.bracesmatching/src/org/netbeans/modules/editor/bracesmatching/BracesMatchHighlighting.java b/editor.bracesmatching/src/org/netbeans/modules/editor/bracesmatching/BracesMatchHighlighting.java --- a/editor.bracesmatching/src/org/netbeans/modules/editor/bracesmatching/BracesMatchHighlighting.java +++ b/editor.bracesmatching/src/org/netbeans/modules/editor/bracesmatching/BracesMatchHighlighting.java @@ -67,6 +67,7 @@ import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.settings.FontColorSettings; import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory; +import org.netbeans.spi.editor.highlighting.ReleasableHighlightsContainer; import org.netbeans.spi.editor.highlighting.ZOrder; /** @@ -74,7 +75,7 @@ * @author Vita Stejskal */ public class BracesMatchHighlighting extends AbstractHighlightsContainer - implements ChangeListener, PropertyChangeListener, HighlightsChangeListener, DocumentListener + implements ReleasableHighlightsContainer, ChangeListener, PropertyChangeListener, HighlightsChangeListener, DocumentListener { private static final Logger LOG = Logger.getLogger(BracesMatchHighlighting.class.getName()); @@ -95,6 +96,8 @@ private final AttributeSet bracesMismatchColoring; private final AttributeSet bracesMatchMulticharColoring; private final AttributeSet bracesMismatchMulticharColoring; + + private boolean released; public BracesMatchHighlighting(JTextComponent component, Document document) { this.document = document; @@ -220,6 +223,10 @@ // ------------------------------------------------ private void refresh() { + if (released) { + return; // No longer notify the matcher since it would leak to memory leak of MasterMatcher.lastResult + } + Caret c = this.caret; if (c == null) { bag.clear(); @@ -247,6 +254,14 @@ } return mimeType; } + + @Override + public void released() { + released = true; +// component.removePropertyChangeListener(this); +// bag.removeHighlightsChangeListener(this); +// document.removeDocumentListener(this); + } public static final class Factory implements HighlightsLayerFactory { public HighlightsLayer[] createLayers(Context context) { diff --git a/editor.bracesmatching/src/org/netbeans/modules/editor/bracesmatching/MasterMatcher.java b/editor.bracesmatching/src/org/netbeans/modules/editor/bracesmatching/MasterMatcher.java --- a/editor.bracesmatching/src/org/netbeans/modules/editor/bracesmatching/MasterMatcher.java +++ b/editor.bracesmatching/src/org/netbeans/modules/editor/bracesmatching/MasterMatcher.java @@ -31,6 +31,8 @@ package org.netbeans.modules.editor.bracesmatching; import java.awt.Color; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -254,6 +256,25 @@ private MasterMatcher(JTextComponent component) { this.component = component; + component.addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if ("document".equals(evt.getPropertyName())) { + synchronized (LOCK) { + // Cancel any pending task and clear the lastResult + if (task != null) { + task.cancel(); + task = null; + } + if (lastResult != null) { + System.err.println("thisMatcher=" + this + "\n lastResult=" + lastResult); + lastResult.cancel(); + lastResult = null; // Prevent memory leak upon document change + } + } + } + } + }); } private Object getAllowedDirection() { diff --git a/editor.lib2/apichanges.xml b/editor.lib2/apichanges.xml --- a/editor.lib2/apichanges.xml +++ b/editor.lib2/apichanges.xml @@ -107,6 +107,19 @@ + + ReleasableHighlightsContainer added + + + + + +

+ Allows highlights containers to be notified that they are no longer being used by highlighting manager. +

+
+ +
Document handling split diff --git a/editor.lib2/manifest.mf b/editor.lib2/manifest.mf --- a/editor.lib2/manifest.mf +++ b/editor.lib2/manifest.mf @@ -1,6 +1,6 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.editor.lib2/1 -OpenIDE-Module-Implementation-Version: 43 +OpenIDE-Module-Implementation-Version: 44 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/editor/lib2/Bundle.properties OpenIDE-Module-Layer: org/netbeans/modules/editor/lib2/resources/layer.xml OpenIDE-Module-Needs: org.netbeans.modules.editor.actions diff --git a/editor.lib2/nbproject/project.properties b/editor.lib2/nbproject/project.properties --- a/editor.lib2/nbproject/project.properties +++ b/editor.lib2/nbproject/project.properties @@ -43,7 +43,7 @@ is.autoload=true javac.source=1.7 javac.compilerargs=-Xlint:unchecked -spec.version.base=2.3.0 +spec.version.base=2.4.0 javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainer.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainer.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainer.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainer.java @@ -56,6 +56,7 @@ import org.netbeans.spi.editor.highlighting.HighlightsChangeListener; import org.netbeans.spi.editor.highlighting.HighlightsContainer; import org.netbeans.spi.editor.highlighting.HighlightsSequence; +import org.netbeans.spi.editor.highlighting.ReleasableHighlightsContainer; import org.openide.util.WeakListeners; /** @@ -67,7 +68,7 @@ * * @author Miloslav Metelka */ -public final class DirectMergeContainer implements HighlightsContainer, HighlightsChangeListener { +public final class DirectMergeContainer implements HighlightsContainer, HighlightsChangeListener, ReleasableHighlightsContainer { // -J-Dorg.netbeans.modules.editor.lib2.highlighting.DirectMergeContainer.level=FINE private static final Logger LOG = Logger.getLogger(DirectMergeContainer.class.getName()); @@ -166,6 +167,15 @@ } return sb.toString(); } + + @Override + public void released() { + for (HighlightsContainer layer : layers) { + if (layer instanceof ReleasableHighlightsContainer) { + ((ReleasableHighlightsContainer) layer).released(); + } + } + } static final class HlSequence implements HighlightsSequenceEx { diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManager.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManager.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManager.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManager.java @@ -69,6 +69,7 @@ import org.netbeans.spi.editor.highlighting.HighlightsContainer; import org.netbeans.spi.editor.highlighting.HighlightsLayer; import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory; +import org.netbeans.spi.editor.highlighting.ReleasableHighlightsContainer; import org.openide.ErrorManager; import org.openide.util.Lookup; import org.openide.util.LookupEvent; @@ -382,6 +383,8 @@ if (inRebuildAllLayers) { return; } + DirectMergeContainer origTopHighlights; + DirectMergeContainer origBottomHighlights; inRebuildAllLayers = true; try { Document doc = pane.getDocument(); @@ -418,7 +421,18 @@ sortedLayers = sl; } // Filter layers by pane's filter - retains order + List origSortedLayers = sortedLayers; sortedLayers = paneFilter.filterLayers(sortedLayers); + if (origSortedLayers.size() != sortedLayers.size()) { // Release filtered out layers + origSortedLayers = new ArrayList<>(origSortedLayers); + origSortedLayers.removeAll(sortedLayers); + for (HighlightsLayer layer : origSortedLayers) { + HighlightsContainer container = HighlightingSpiPackageAccessor.get().getHighlightsLayerAccessor(layer).getContainer(); + if (container instanceof ReleasableHighlightsContainer) { + ((ReleasableHighlightsContainer)container).released(); + } + } + } int topStartIndex = 0; for (int i = 0; i < sortedLayers.size(); i++) { @@ -454,6 +468,8 @@ topContainers = Collections.emptyList(); } + origBottomHighlights = bottomHighlights; + origTopHighlights = topHighlights; bottomHighlights = new DirectMergeContainer(bottomContainers.toArray( new HighlightsContainer[bottomContainers.size()])); topHighlights = new DirectMergeContainer(topContainers.toArray( @@ -461,6 +477,12 @@ } finally { inRebuildAllLayers = false; } + if (origBottomHighlights != null) { + origBottomHighlights.released(); + } + if (origTopHighlights != null) { + origTopHighlights.released(); + } fireChangeListeners(); } diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java @@ -610,6 +610,7 @@ viewHierarchyImpl.setDocumentView(null); uninstallFromViewport(); textComponent.removePropertyChangeListener(this); + viewUpdates.released(); viewUpdates = null; } diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactory.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactory.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactory.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactory.java @@ -96,9 +96,9 @@ return viewFactoryFactories; } - private final DocumentView docView; + private DocumentView docView; - private final JTextComponent component; + private JTextComponent component; private ViewBuilder viewBuilder; @@ -115,7 +115,7 @@ /** * Text component for which this view factory was constructed. * - * @return non-null text component. + * @return text component or null if this view factory is released. */ protected final JTextComponent textComponent() { return component; @@ -127,10 +127,11 @@ * it may differ from document() result at certain points * and it could lead to incorrect behavior. * - * @return non-null document for which the view hierarchy was constructed. + * @return document for which the view hierarchy was constructed + * or null if this view factory is released. */ protected final Document document() { - return docView.getDocument(); + return (docView != null) ? docView.getDocument() : null; } /** @@ -295,6 +296,37 @@ this.viewBuilder = viewBuilder; } + public final boolean isReleased() { + return (docView == null); + } + + /** + * Notification that this factory is no longer being used so it should + * release its resources - for example detach all listeners. + *
+ * It's called upon document view receives setParent(null) which typically signals + * that a new document view will be created for the particular editor pane. + */ + protected void released() { + } + + void releaseAll() { + released(); // Is allowed to use docView and component info before subsequent clearing + this.docView = null; + this.component = null; + // this.viewBuilder should be null-ed in try..finally manner + } + + /** + * Used by highlights view factory. If there would be a high demand for this method + * it might become public although the view factories are used at times when + * the document view is not in a "stable" state so the factories would have to be careful. + * @return + */ + DocumentView documentView() { + return docView; + } + @Override public String toString() { ViewBuilder vb = viewBuilder; diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java @@ -108,8 +108,6 @@ private static final int RTL_CHAR_TYPE = 2; private static final int TAB_CHAR_TYPE = 3; - private final DocumentView docView; - private final HighlightingManager highlightingManager; private HighlightsContainer highlightsContainer; @@ -154,11 +152,13 @@ public HighlightsViewFactory(View documentView) { super(documentView); - this.docView = (DocumentView) documentView; highlightingManager = HighlightingManager.getInstance(textComponent()); highlightingManager.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { // Layers in highlighting manager changed + if (isReleased()) { + return; + } notifyStaleCreation(); updateHighlightsContainer(); fireEvent(EditorViewFactoryChange.createList(0, document().getLength() + 1, @@ -260,7 +260,7 @@ TextLayout origTextLayout = origHView.getTextLayout(); if (origTextLayout != null) { if (ViewHierarchyImpl.CHECK_LOG.isLoggable(Level.FINE)) { - String origText = docView.getTextLayoutVerifier().get(origTextLayout); + String origText = documentView().getTextLayoutVerifier().get(origTextLayout); if (origText != null) { CharSequence text = docText.subSequence(startOffset, startOffset + length); if (!CharSequenceUtilities.textEquals(text, origText)) { @@ -360,6 +360,9 @@ @Override public void highlightChanged(final HighlightsChangeEvent evt) { + if (isReleased()) { + return; + } // Since still many highlighting layers fire changes without document lock acquired // do an extra read lock so that view hierarchy surely operates under document lock document().render(new Runnable() { diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewUpdates.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewUpdates.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewUpdates.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewUpdates.java @@ -168,6 +168,14 @@ new Object[] { docView.getTextComponent(), Arrays.asList(viewFactories) }); } } + + void released() { + if (viewFactories != null) { + for (EditorViewFactory viewFactory : viewFactories) { + viewFactory.releaseAll(); + } + } + } /** * Start view building process (it must be followed by finishBuildViews() in try-finally). diff --git a/editor.lib2/src/org/netbeans/spi/editor/highlighting/ReleasableHighlightsContainer.java b/editor.lib2/src/org/netbeans/spi/editor/highlighting/ReleasableHighlightsContainer.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/spi/editor/highlighting/ReleasableHighlightsContainer.java @@ -0,0 +1,59 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2015 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 2015 Sun Microsystems, Inc. + */ +package org.netbeans.spi.editor.highlighting; + +/** + * Highlights container that wishes to free its resources once the highlighting manager + * stops using it. + * + * @author Miloslav Metelka + * @since 2.4 + */ +public interface ReleasableHighlightsContainer extends HighlightsContainer { + + /** + * Called once the highlighting manager rebuilds its layers so this + * layer is no longer being used. + */ + void released(); + +} diff --git a/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManagerTest.java b/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManagerTest.java --- a/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManagerTest.java +++ b/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManagerTest.java @@ -70,7 +70,9 @@ import org.netbeans.spi.editor.highlighting.HighlightsLayer; import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory; import org.netbeans.spi.editor.highlighting.HighlightsSequence; +import org.netbeans.spi.editor.highlighting.ReleasableHighlightsContainer; import org.netbeans.spi.editor.highlighting.ZOrder; +import org.netbeans.spi.editor.highlighting.support.AbstractHighlightsContainer; import org.openide.util.Lookup; /** @@ -351,7 +353,7 @@ bagB.addHighlight(15, 25, attribsB); bagC.addHighlight(50, 60, attribsC); bagD.addHighlight(55, 65, attribsD); - + SingletonLayerFactory layerA = new SingletonLayerFactory("layerA", ZOrder.DEFAULT_RACK, true, bagA); SingletonLayerFactory layerB = new SingletonLayerFactory("layerB", ZOrder.DEFAULT_RACK.forPosition(1), false, bagB); SingletonLayerFactory layerC = new SingletonLayerFactory("layerC", ZOrder.DEFAULT_RACK.forPosition(2), true, bagC); @@ -634,6 +636,24 @@ assertFalse("There should be no highlights", fixed.moveNext()); } } + + public void testReleaseLayers() { + final String mimeType = "text/plain"; + TestReleaseHighlightsContainer releasableContainer = new TestReleaseHighlightsContainer(); + SingletonLayerFactory releasableLayer = new SingletonLayerFactory( + "releasableLayer", ZOrder.DEFAULT_RACK.forPosition(1), true, releasableContainer); + + MemoryMimeDataProvider.reset(null); + MemoryMimeDataProvider.addInstances(mimeType, releasableLayer); + + JEditorPane pane = new JEditorPane(mimeType, "Hello"); + HighlightingManager hm = HighlightingManager.getInstance(pane); // Ensure layers get created + hm.getHighlights(HighlightsLayerFilter.IDENTITY); + pane.setEditorKit(new SimpleKit(mimeType)); + // Do not check against concrete release count since there are e.g. mime lookup rebuild notifications + // that lead to HM.rebuildAllLayers() which increases the releaseCount too. + assertTrue("Highlights container releasing not performed after pane.setEditorKit()", releasableContainer.releaseCount > 0); + } // test getting independent HCs for different JEditorPanes with the same mime type @@ -870,4 +890,23 @@ return filteredLayers; } }; + + private static final class TestReleaseHighlightsContainer extends AbstractHighlightsContainer + implements ReleasableHighlightsContainer + { + + public int releaseCount; + + @Override + public HighlightsSequence getHighlights(int startOffset, int endOffset) { + return HighlightsSequence.EMPTY; + } + + @Override + public void released() { + releaseCount++; + } + + } + }