--- a/editor.lib/src/org/netbeans/editor/BaseDocument.java +++ a/editor.lib/src/org/netbeans/editor/BaseDocument.java @@ -574,7 +574,6 @@ this.addUpdateDocumentListener(modElementRoot); modElementRoot.setEnabled(true); - TrailingWhitespaceRemove.ensureRegistered(); BeforeSaveTasks.get(this); // Ensure that "beforeSaveRunnable" gets initialized undoEditWrappers = MimeLookup.getLookup(mimeType).lookupAll(UndoableEditWrapper.class); --- a/editor.lib/src/org/netbeans/modules/editor/lib/BeforeSaveTasks.java +++ a/editor.lib/src/org/netbeans/modules/editor/lib/BeforeSaveTasks.java @@ -45,12 +45,17 @@ package org.netbeans.modules.editor.lib; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import javax.swing.text.Document; import javax.swing.undo.UndoableEdit; +import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.editor.BaseDocument; +import org.netbeans.lib.editor.util.swing.DocumentUtilities; +import org.netbeans.modules.editor.lib2.document.DocumentSpiPackageAccessor; +import org.netbeans.modules.editor.lib2.document.ModRootElement; +import org.netbeans.spi.editor.document.OnSaveTask; /** * Registration of tasks performed right before document save. @@ -62,8 +67,6 @@ private static final Logger LOG = Logger.getLogger(BeforeSaveTasks.class.getName()); - private static final List tasks = new ArrayList(5); - public static synchronized BeforeSaveTasks get(BaseDocument doc) { BeforeSaveTasks beforeSaveTasks = (BeforeSaveTasks) doc.getProperty(BeforeSaveTasks.class); if (beforeSaveTasks == null) { @@ -91,130 +94,77 @@ doc.putProperty("beforeSaveRunnable", beforeSaveRunnable); // NOI18N } - /** - * Add a new task to be executed before save of the document. - * - * @param task non-null task. - */ - public static void addTask(Task task) { - if (task == null) - throw new IllegalArgumentException("task must not be null"); // NOI18N - synchronized (tasks) { - tasks.add(task); + void runTasks() { + String mimeType = DocumentUtilities.getMimeType(doc); + Collection factories = MimeLookup.getLookup(mimeType). + lookupAll(OnSaveTask.Factory.class); + OnSaveTask.Context context = DocumentSpiPackageAccessor.get().createContext(doc); + List tasks = new ArrayList(factories.size()); + for (OnSaveTask.Factory factory : factories) { + OnSaveTask task = factory.createTask(context); + if (task != null) { + tasks.add(task); + } + } + if (tasks.size() > 0) { + new TaskRunnable(doc, tasks, context).run(); } } - /** - * Remove a task from the list of existing before-save tasks. - * - * @param task runnable to be removed. - * @return true if the tasks was removed successfully or false if the task - * was not found (compared by Object.equals()). - */ - public static boolean removeTask(Task task) { - synchronized (tasks) { - return tasks.remove(task); + private static final class TaskRunnable implements Runnable { + + final BaseDocument doc; + + final List tasks; + + final OnSaveTask.Context context; + + int lockedTaskIndex; + + public TaskRunnable(BaseDocument doc, List tasks, OnSaveTask.Context context) { + this.doc = doc; + this.tasks = tasks; + this.context = context; } - } - - public static boolean removeTask(Class taskClass) { - synchronized (tasks) { - int i = taskIndex(taskClass); - if (i >= 0) { - tasks.remove(i); - return true; + + @Override + public void run() { + if (lockedTaskIndex < tasks.size()) { + OnSaveTask task = tasks.get(lockedTaskIndex++); + task.runLocked(this); + + } else { + try { + doc.runAtomicAsUser(new Runnable() { + public @Override + void run() { + UndoableEdit atomicEdit = EditorPackageAccessor.get().BaseDocument_markAtomicEditsNonSignificant(doc); + DocumentSpiPackageAccessor.get().setUndoEdit(context, atomicEdit); + + // Since these are before-save actions they should generally not prevent + // the save operation to succeed. Thus the possible exceptions thrown + // by the tasks will be notified but they will not prevent the save to succeed. + try { + for (int i = 0; i < tasks.size(); i++) { + OnSaveTask task = tasks.get(i); + DocumentSpiPackageAccessor.get().setTaskStarted(context, true); + task.performTask(); + } + ModRootElement modRootElement = ModRootElement.get(doc); + if (modRootElement != null) { + modRootElement.resetMods(atomicEdit); + } + } catch (Exception e) { + LOG.log(Level.WARNING, "Exception thrown in before-save tasks", e); // NOI18N + } + + } + }); + } finally { + EditorPackageAccessor.get().BaseDocument_clearAtomicEdits(doc); + } } } - return false; - } - - public static Task getTask(Class taskClass) { - synchronized (tasks) { - int i = taskIndex(taskClass); - return (i >= 0) ? tasks.get(i) : null; - } - } - - private static int taskIndex(Class taskClass) { - for (int i = tasks.size() - 1; i >= 0; i--) { - Task task = tasks.get(i); - if (taskClass == task.getClass()) { - return i; - } - } - return -1; - } - - void runTasks() { - final List tasksCopy = new ArrayList(tasks); - final List locks = new ArrayList(tasksCopy.size()); - int taskCount = tasksCopy.size(); - int lockedTaskEndIndex = 0; - for (;lockedTaskEndIndex < taskCount; lockedTaskEndIndex++) { - Task task = tasksCopy.get(lockedTaskEndIndex); - locks.add(task.lock(doc)); - } - try { - doc.runAtomicAsUser (new Runnable () { - public @Override void run () { - UndoableEdit atomicEdit = EditorPackageAccessor.get().BaseDocument_markAtomicEditsNonSignificant(doc); - // Since these are before-save actions they should generally not prevent - // the save operation to succeed. Thus the possible exceptions thrown - // by the tasks will be notified but they will not prevent the save to succeed. - try { - for (int i = 0; i < tasksCopy.size(); i++) { - Task task = tasksCopy.get(i); - task.run(locks.get(i), doc, atomicEdit); - } - } catch (Exception e) { - LOG.log(Level.WARNING, "Exception thrown in before-save tasks", e); // NOI18N - } - - } - }); - } finally { - while (lockedTaskEndIndex > 0) { - Task task = tasksCopy.get(--lockedTaskEndIndex); - task.unlock(locks.get(lockedTaskEndIndex), doc); - } - - EditorPackageAccessor.get().BaseDocument_clearAtomicEdits(doc); - } - } - - /** - * Task run right before document saving such as reformatting or trailing whitespace removal. - */ - public interface Task { - - /** - * Perform an extra lock for task if necessary. - * Locks for all tasks will be obtained first then atomic document lock will be obtained - * and then all the tasks will be run. Then all the locks will be released by running unlock() - * in all tasks in a reverse order of locking. - * @param doc non-null document. - */ - Object lock(Document doc); - - /** - * Run the before-save task. - * - * @param lockInfo lock object produced by {@link #lock(javax.swing.text.Document) } - * that may contain an arbitrary info. - * @param doc non-null document. - * @param edit a non-ended edit to which undoable edits of the task should be added. - * It may be null in which case the produced tasks should not be added to anything. - */ - void run(Object lockInfo, Document doc, UndoableEdit edit); - - /** - * Perform an extra unlock for task if necessary. - * - * @param lockInfo lock object produced by {@link #lock(javax.swing.text.Document) } - * that may contain an arbitrary info. - * @param doc non-null document. - */ - void unlock(Object lockInfo, Document doc); } --- a/editor.lib/src/org/netbeans/modules/editor/lib/TrailingWhitespaceRemove.java +++ a/editor.lib/src/org/netbeans/modules/editor/lib/TrailingWhitespaceRemove.java @@ -44,16 +44,19 @@ package org.netbeans.modules.editor.lib; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; import java.util.prefs.Preferences; import javax.swing.text.Document; import javax.swing.undo.UndoableEdit; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.api.editor.mimelookup.MimeRegistration; import org.netbeans.api.editor.settings.SimpleValueNames; import org.netbeans.lib.editor.util.swing.DocumentUtilities; import org.netbeans.modules.editor.lib2.document.ModRootElement; import org.netbeans.modules.editor.lib2.document.TrailingWhitespaceRemoveProcessor; +import org.netbeans.spi.editor.document.OnSaveTask; /** * Removal of trailing whitespace @@ -61,29 +64,21 @@ * @author Miloslav Metelka * @since 1.27 */ -public final class TrailingWhitespaceRemove implements BeforeSaveTasks.Task { +public final class TrailingWhitespaceRemove implements OnSaveTask { // -J-Dorg.netbeans.modules.editor.lib.TrailingWhitespaceRemove.level=FINE static final Logger LOG = Logger.getLogger(TrailingWhitespaceRemove.class.getName()); - private static final TrailingWhitespaceRemove INSTANCE = new TrailingWhitespaceRemove(); + private final Document doc; + + private AtomicBoolean canceled = new AtomicBoolean(); - public static void ensureRegistered() { - if (BeforeSaveTasks.getTask(TrailingWhitespaceRemove.class) == null) { - BeforeSaveTasks.addTask(INSTANCE); - } - } - - private TrailingWhitespaceRemove() { + TrailingWhitespaceRemove(Document doc) { + this.doc = doc; } @Override - public Object lock(Document doc) { - return null; // No extra locking - } - - @Override - public void run(Object lock, Document doc, UndoableEdit compoundEdit) { + public void performTask() { Preferences prefs = MimeLookup.getLookup(DocumentUtilities.getMimeType(doc)).lookup(Preferences.class); if (prefs.getBoolean(SimpleValueNames.ON_SAVE_USE_GLOBAL_SETTINGS, Boolean.TRUE)) { prefs = MimeLookup.getLookup(MimePath.EMPTY).lookup(Preferences.class); @@ -95,7 +90,7 @@ boolean origEnabled = modRootElement.isEnabled(); modRootElement.setEnabled(false); try { - new TrailingWhitespaceRemoveProcessor(doc, "modified-lines".equals(policy)).removeWhitespace(); //NOI18N + new TrailingWhitespaceRemoveProcessor(doc, "modified-lines".equals(policy), canceled).removeWhitespace(); //NOI18N } finally { modRootElement.setEnabled(origEnabled); } @@ -104,7 +99,24 @@ } @Override - public void unlock(Object lock, Document doc) { + public void runLocked(Runnable run) { + run.run(); + } + + @Override + public boolean cancel() { + canceled.set(true); + return true; + } + + @MimeRegistration(mimeType="", service=OnSaveTask.Factory.class, position=1000) + public static final class FactoryImpl implements Factory { + + @Override + public OnSaveTask createTask(Context context) { + return new TrailingWhitespaceRemove(context.getDocument()); + } + } } --- a/editor.lib/test/unit/src/org/netbeans/modules/editor/lib/BeforeSaveTasksTest.java +++ a/editor.lib/test/unit/src/org/netbeans/modules/editor/lib/BeforeSaveTasksTest.java @@ -0,0 +1,169 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2012 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 2012 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.lib; + +import javax.swing.undo.UndoableEdit; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.api.editor.mimelookup.test.MockMimeLookup; +import org.netbeans.editor.BaseDocument; +import org.netbeans.junit.MockServices; +import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.editor.document.OnSaveTask; + +/** + * + * @author Miloslav Metelka + */ +public class BeforeSaveTasksTest extends NbTestCase { + + private static final String MIME_TYPE = "text/x-test-on-save"; + + public BeforeSaveTasksTest(String name) { + super(name); + } + + public void testOnSaveTasks() { + MockServices.setServices(MockMimeLookup.class); + MockMimeLookup.setInstances(MimePath.parse(MIME_TYPE), + new TestOnSaveTask1.TestFactory1(), + new TestOnSaveTask2.TestFactory2() + ); + BaseDocument doc = new BaseDocument(false, MIME_TYPE); + BeforeSaveTasks.get(doc); + Runnable beforeSaveRunnable = (Runnable) doc.getProperty("beforeSaveRunnable"); + beforeSaveRunnable.run(); + assertNotNull("TestOnSaveTask2 not created", TestOnSaveTask2.TestFactory2.lastCreatedTask); + assertTrue("TestOnSaveTask2 not run", TestOnSaveTask2.TestFactory2.lastCreatedTask.taskPerformed); + } + + private static final class TestOnSaveTask1 implements OnSaveTask { + + boolean taskLocked; + + boolean taskPerformed; + + TestOnSaveTask1(Context context) { + } + + @Override + public void performTask() { + assertTrue("Task not locked", taskLocked); + assertFalse("Task run multiple times", taskPerformed); + taskPerformed = true; + } + + @Override + public void runLocked(Runnable run) { + taskLocked = true; + try { + run.run(); + } finally { + taskLocked = false; + } + } + + @Override + public boolean cancel() { + return true; + } + + static final class TestFactory1 implements OnSaveTask.Factory { + + static TestOnSaveTask1 lastCreatedTask; + + public OnSaveTask createTask(Context context) { + assertNotNull("Context null", context); + return (lastCreatedTask = new TestOnSaveTask1(context)); + } + + } + + } + + private static final class TestOnSaveTask2 implements OnSaveTask { + + boolean taskLocked; + + boolean taskPerformed; + + TestOnSaveTask2(OnSaveTask.Context context) { + } + + @Override + public void performTask() { + assertTrue("Task1 not locked", TestOnSaveTask1.TestFactory1.lastCreatedTask.taskLocked); + assertTrue("Task1 not performed yet", TestOnSaveTask1.TestFactory1.lastCreatedTask.taskPerformed); + + assertTrue("Task not locked", taskLocked); + assertFalse("Task run multiple times", taskPerformed); + taskPerformed = true; + } + + @Override + public void runLocked(Runnable run) { + taskLocked = true; + try { + run.run(); + } finally { + taskLocked = false; + } + } + + @Override + public boolean cancel() { + return true; + } + + static final class TestFactory2 implements OnSaveTask.Factory { + + static TestOnSaveTask2 lastCreatedTask; + + public OnSaveTask createTask(OnSaveTask.Context context) { + assertNotNull("Context null", context); + return (lastCreatedTask = new TestOnSaveTask2(context)); + } + + } + + } + +} --- a/editor.lib/test/unit/src/org/netbeans/modules/editor/lib/TrailingWhitespaceRemoveTest.java +++ a/editor.lib/test/unit/src/org/netbeans/modules/editor/lib/TrailingWhitespaceRemoveTest.java @@ -52,9 +52,11 @@ import javax.swing.undo.UndoManager; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.api.editor.mimelookup.test.MockMimeLookup; import org.netbeans.api.editor.settings.SimpleValueNames; import org.netbeans.editor.BaseDocument; import org.netbeans.editor.BaseKit; +import org.netbeans.junit.MockServices; import org.netbeans.junit.NbTestCase; import org.netbeans.lib.editor.util.ArrayUtilities; import org.netbeans.lib.editor.util.CharSequenceUtilities; @@ -94,6 +96,11 @@ } private void checkTrailingWhitespaceRemove(String policy, String result) throws Exception { + MockServices.setServices(MockMimeLookup.class); + MockMimeLookup.setInstances(MimePath.parse(""), + new TrailingWhitespaceRemove.FactoryImpl() + ); + RandomTestContainer container = DocumentTesting.initContainer(null); container.setName(this.getName()); // container.putProperty(RandomTestContainer.LOG_OP, Boolean.TRUE); --- a/editor.lib2/apichanges.xml +++ a/editor.lib2/apichanges.xml @@ -107,6 +107,21 @@ + + OnSaveTask interface added + + + + + +

+ Added OnSaveTask interface which allows modules to register + tasks into MimeLookup that will be performed right before document saving. +

+
+ +
+ UndoableEditWrapper interface added --- a/editor.lib2/manifest.mf +++ a/editor.lib2/manifest.mf @@ -1,6 +1,6 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.editor.lib2/1 -OpenIDE-Module-Implementation-Version: 33 +OpenIDE-Module-Implementation-Version: 34 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 --- a/editor.lib2/nbproject/project.properties +++ a/editor.lib2/nbproject/project.properties @@ -43,7 +43,7 @@ is.autoload=true javac.source=1.6 javac.compilerargs=-Xlint:unchecked -spec.version.base=1.65.0 +spec.version.base=1.66.0 javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml --- a/editor.lib2/nbproject/project.xml +++ a/editor.lib2/nbproject/project.xml @@ -148,6 +148,12 @@ + org.netbeans.modules.editor.mimelookup + + + + + org.netbeans.modules.editor.mimelookup.impl --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/document/DocumentInternalUtils.java +++ a/editor.lib2/src/org/netbeans/modules/editor/lib2/document/DocumentInternalUtils.java @@ -56,14 +56,15 @@ } public static Element customElement(Document doc, int startOffset, int endOffset) { - return new CustomElement(doc, startOffset, endOffset); + return new CustomRootElement(doc, startOffset, endOffset); } private static final class CustomElement extends AbstractPositionElement { - CustomElement(Document doc, int startOffset, int endOffset) { - super(new CustomRootElement(doc), startOffset, endOffset); - CharSequenceUtilities.checkIndexesValid(startOffset, endOffset, doc.getLength() + 1); + CustomElement(Element parent, int startOffset, int endOffset) { + super(parent, startOffset, endOffset); + CharSequenceUtilities.checkIndexesValid(startOffset, endOffset, + parent.getDocument().getLength() + 1); } @Override @@ -76,8 +77,11 @@ private static final class CustomRootElement extends AbstractRootElement { - public CustomRootElement(Document doc) { + private final CustomElement customElement; + + public CustomRootElement(Document doc, int startOffset, int endOffset) { super(doc); + customElement = new CustomElement(this, startOffset, endOffset); } @Override @@ -85,5 +89,19 @@ return "CustomRootElement"; } + @Override + public Element getElement(int index) { + if (index == 0) { + return customElement; + } else { + return null; + } + } + + @Override + public int getElementCount() { + return 1; + } + } } --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/document/DocumentSpiPackageAccessor.java +++ a/editor.lib2/src/org/netbeans/modules/editor/lib2/document/DocumentSpiPackageAccessor.java @@ -0,0 +1,81 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2012 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 2012 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.lib2.document; + +import javax.swing.text.Document; +import javax.swing.undo.UndoableEdit; +import org.netbeans.spi.editor.document.OnSaveTask; +import org.openide.util.Exceptions; + +/** + * Package accessor for o.n.spi.editor.document package. + * + * @author Miloslav Metelka + */ +public abstract class DocumentSpiPackageAccessor { + + private static DocumentSpiPackageAccessor INSTANCE; + + public static DocumentSpiPackageAccessor get() { + if (INSTANCE == null) { + // Cause api accessor impl to get initialized + try { + Class.forName(OnSaveTask.Context.class.getName(), true, DocumentSpiPackageAccessor.class.getClassLoader()); + } catch (ClassNotFoundException e) { + Exceptions.printStackTrace(e); + } + assert (INSTANCE != null) : "Registration failed"; // NOI18N + } + return INSTANCE; + } + + public static void register(DocumentSpiPackageAccessor accessor) { + INSTANCE = accessor; + } + + public abstract OnSaveTask.Context createContext(Document doc); + + public abstract void setUndoEdit(OnSaveTask.Context context, UndoableEdit undoEdit); + + public abstract void setTaskStarted(OnSaveTask.Context context, boolean taskStarted); + +} --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/document/TrailingWhitespaceRemoveProcessor.java +++ a/editor.lib2/src/org/netbeans/modules/editor/lib2/document/TrailingWhitespaceRemoveProcessor.java @@ -41,6 +41,7 @@ */ package org.netbeans.modules.editor.lib2.document; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.text.BadLocationException; @@ -140,10 +141,13 @@ * Shift offset of the caret relative to caretLineIndex's line beginning. */ private final int caretRelativeOffset; + + private final AtomicBoolean canceled; - public TrailingWhitespaceRemoveProcessor(Document doc, boolean removeFromModifiedLinesOnly) { + public TrailingWhitespaceRemoveProcessor(Document doc, boolean removeFromModifiedLinesOnly, AtomicBoolean canceled) { this.doc = doc; this.removeFromModifiedLinesOnly = removeFromModifiedLinesOnly; + this.canceled = canceled; this.docText = DocumentUtilities.getText(doc); // Persists for doc's lifetime lineRootElement = DocumentUtilities.getParagraphRootElement(doc); modRootElement = ModRootElement.get(doc); --- a/editor.lib2/src/org/netbeans/spi/editor/document/OnSaveTask.java +++ a/editor.lib2/src/org/netbeans/spi/editor/document/OnSaveTask.java @@ -0,0 +1,198 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2012 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 2012 Sun Microsystems, Inc. + */ +package org.netbeans.spi.editor.document; + +import javax.swing.text.Document; +import javax.swing.text.Element; +import javax.swing.undo.UndoableEdit; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.modules.editor.lib2.document.DocumentSpiPackageAccessor; +import org.netbeans.modules.editor.lib2.document.ModRootElement; +import org.netbeans.spi.editor.mimelookup.MimeLocation; +import org.openide.util.Cancellable; + +/** + * Task done right before document is saved. + * Factories need to be registered in MimeLookup. + * + * @author Miloslav Metelka + * @since 1.66 + */ +public interface OnSaveTask extends Cancellable { + + /** + * Perform the task on the context (given to factory that produced this task). + */ + void performTask(); + + /** + * Perform the given runnable under a lock specific for this task. + * The runnable will include a call to {@link #performTask() } but it may + * also call other tasks if there are multiple ones. + *
+ * For multiple task factories registered their {@link #runLocked(java.lang.Runnable) } + * methods will be called subsequently (according to their registration order) in a nested way. + * + * @param run non-null runnable provided by the infrastructure. + */ + void runLocked(@NonNull Runnable run); + + /** + * Cancel processing of the job. Called not more than once for specific job. + * + * @return true if the job was successfully canceled, false if job + * can't be canceled for some reason. + * @see Cancellable + */ + public boolean cancel(); + + /** + * Factory for creation of on-save task. + * It should be registered in MimeLookup via xml layer in "/Editors/<mime-type>" + * folder. + */ + @MimeLocation(subfolderName="OnSave") + public interface Factory { + + /** + * Create on-save task. + * + * @param context non-null context containing info for the task. + * @return task instance or null if the task is not appropriate for the given context. + */ + OnSaveTask createTask(@NonNull Context context); + + } + + /** + * Context given to factory for production of on-save task. + */ + public static final class Context { + + static { + DocumentSpiPackageAccessor.register(new PackageAccessor()); + } + + private final Document doc; + + private UndoableEdit undoEdit; + + private boolean taskStarted; + + Context(Document doc) { + this.doc = doc; + } + + /** + * Get a document on which the task is being executed. + * @return + */ + public Document getDocument() { + return doc; + } + + + /** + * Task may add a custom undoable edit related to its operation by using this method. + *
+ * When undo would be performed after the save then this edit would be undone + * (together with any possible modification changes performed by the task on the underlying document). + *
+ * Note: this method should only be called during {@link OnSaveTask#performTask() }. + * + * @param edit a custom undoable edit provided by the task. + */ + public void addUndoEdit(UndoableEdit edit) { + if (!taskStarted) { + throw new IllegalStateException("This method may only be called during OnSaveTask.performTask()"); + } + if (undoEdit != null) { + undoEdit.addEdit(edit); + } + } + + /** + * Get a root element with zero or more child elements each designating a modified region + * of a document. + *
+ * Tasks may use this information to work on modified document parts only. + *
+ * Note: unlike in some other root element implementations here the child elements + * do not fully cover the root element's offset space. + * + * @return root element containing modified regions of document as child elements. + * Null if document's implementation does not support modified elements collecting. + */ + public Element getModificationsRootElement() { + return ModRootElement.get(doc); + } + + void setUndoEdit(UndoableEdit undoEdit) { + this.undoEdit = undoEdit; + } + + void setTaskStarted(boolean taskStarted) { + this.taskStarted = taskStarted; + } + + } + + static final class PackageAccessor extends DocumentSpiPackageAccessor { + + @Override + public Context createContext(Document doc) { + return new Context(doc); + } + + @Override + public void setUndoEdit(Context context, UndoableEdit undoEdit) { + context.setUndoEdit(undoEdit); + } + + @Override + public void setTaskStarted(Context context, boolean taskStarted) { + context.setTaskStarted(taskStarted); + } + + } + +} --- a/editor/src/org/netbeans/modules/editor/EditorModule.java +++ a/editor/src/org/netbeans/modules/editor/EditorModule.java @@ -307,8 +307,6 @@ } } }); - - ReformatBeforeSaveTask.ensureRegistered(); } /** Called when module is uninstalled. Overrides superclass method. */ --- a/editor/src/org/netbeans/modules/editor/impl/ReformatBeforeSaveTask.java +++ a/editor/src/org/netbeans/modules/editor/impl/ReformatBeforeSaveTask.java @@ -42,8 +42,8 @@ package org.netbeans.modules.editor.impl; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.Preferences; @@ -54,6 +54,7 @@ import javax.swing.undo.UndoableEdit; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.api.editor.mimelookup.MimeRegistration; import org.netbeans.api.editor.settings.SimpleValueNames; import org.netbeans.editor.GuardedDocument; import org.netbeans.editor.MarkBlock; @@ -62,10 +63,9 @@ import org.netbeans.lib.editor.util.swing.DocumentUtilities; import org.netbeans.lib.editor.util.swing.PositionRegion; import org.netbeans.modules.editor.indent.api.Reformat; -import org.netbeans.modules.editor.lib.BeforeSaveTasks; import org.netbeans.modules.editor.lib2.document.DocumentInternalUtils; import org.netbeans.modules.editor.lib2.document.ModRootElement; -import org.netbeans.modules.editor.lib2.document.TrailingWhitespaceRemoveProcessor; +import org.netbeans.spi.editor.document.OnSaveTask; import org.openide.util.Exceptions; /** @@ -73,195 +73,198 @@ * * @author Miloslav Metelka */ -public class ReformatBeforeSaveTask implements BeforeSaveTasks.Task { - - private static final ReformatBeforeSaveTask INSTANCE = new ReformatBeforeSaveTask(); - - public static void ensureRegistered() { - if (BeforeSaveTasks.getTask(ReformatBeforeSaveTask.class) == null) { - BeforeSaveTasks.addTask(INSTANCE); - } - } +public class ReformatBeforeSaveTask implements OnSaveTask { // -J-Dorg.netbeans.modules.editor.impl.ReformatAtSaveTask.level=FINE private static final Logger LOG = Logger.getLogger(ReformatBeforeSaveTask.class.getName()); - private ReformatBeforeSaveTask() { + private final Document doc; + + private Reformat reformat; + + private boolean modifiedLinesOnly; + + private List guardedBlocks; + + private int guardedBlockIndex; + + private Position guardedBlockStartPos; + + private Position guardedBlockEndPos; + + private AtomicBoolean canceled = new AtomicBoolean(); + + ReformatBeforeSaveTask(Document doc) { + this.doc = doc; } @Override - public Object lock(Document doc) { + public void performTask() { + if (reformat != null) { + reformat(); + } + } + + @Override + public void runLocked(Runnable run) { Preferences prefs = MimeLookup.getLookup(DocumentUtilities.getMimeType(doc)).lookup(Preferences.class); if (prefs.getBoolean(SimpleValueNames.ON_SAVE_USE_GLOBAL_SETTINGS, Boolean.TRUE)) { prefs = MimeLookup.getLookup(MimePath.EMPTY).lookup(Preferences.class); } String policy = prefs.get(SimpleValueNames.ON_SAVE_REFORMAT, "never"); //NOI18N if (!"never".equals(policy)) { //NOI18N - Reformat reformat = Reformat.get(doc); + modifiedLinesOnly = "modified-lines".equals(policy); + reformat = Reformat.get(doc); reformat.lock(); - LockInfo lockInfo = new LockInfo(reformat, "modified-lines".equals(policy)); - return lockInfo; - } - return null; - } - - @Override - public void run(Object lock, Document doc, UndoableEdit compoundEdit) { - if (lock != null) { - ((LockInfo)lock).reformat(doc); + try { + run.run(); + } finally { + reformat.unlock(); + } + } else { + run.run(); } } @Override - public void unlock(Object lock, Document doc) { - if (lock != null) { - ((LockInfo)lock).unlock(); + public boolean cancel() { + canceled.set(true); + return true; + } + + void reformat() { + ModRootElement modRootElement = ModRootElement.get(doc); + if (modRootElement != null) { + boolean origEnabled = modRootElement.isEnabled(); + modRootElement.setEnabled(false); + try { + // Read all guarded blocks + guardedBlocks = new GapList(); + if (doc instanceof GuardedDocument) { + MarkBlock block = ((GuardedDocument) doc).getGuardedBlockChain().getChain(); + while (block != null) { + try { + guardedBlocks.add(new PositionRegion(doc, block.getStartOffset(), block.getEndOffset())); + } catch (BadLocationException ex) { + Exceptions.printStackTrace(ex); + } + block = block.getNext(); + } + + } + + guardedBlockIndex = 0; + fetchNextGuardedBlock(); + Element modRootOrDocElement = (modifiedLinesOnly) + ? modRootElement + : DocumentInternalUtils.customElement(doc, 0, doc.getLength()); + int modElementCount = modRootOrDocElement.getElementCount(); + List formatBlocks = new ArrayList(modElementCount); + for (int i = 0; i < modElementCount; i++) { + if (canceled.get()) { + return; + } + Element modElement = modRootOrDocElement.getElement(i); + boolean modElementFinished; + boolean add; + int startOffset = modElement.getStartOffset(); + int modElementEndOffset = modElement.getEndOffset(); + int endOffset = modElementEndOffset; + do { + if (guardedBlockStartPos != null) { + BlockCompare blockCompare = BlockCompare.get( + startOffset, + endOffset, + guardedBlockStartPos.getOffset(), + guardedBlockEndPos.getOffset()); + if (blockCompare.before()) { + add = true; + modElementFinished = true; + } else if (blockCompare.after()) { + fetchNextGuardedBlock(); + add = false; + modElementFinished = false; + } else if (blockCompare.equal()) { + fetchNextGuardedBlock(); + add = false; + modElementFinished = true; + } else if (blockCompare.overlapStart()) { + endOffset = guardedBlockStartPos.getOffset(); + add = true; + modElementFinished = true; + } else if (blockCompare.overlapEnd()) { + // Skip part covered by guarded block + endOffset = guardedBlockEndPos.getOffset(); + fetchNextGuardedBlock(); + add = false; + modElementFinished = false; + } else if (blockCompare.contains()) { + endOffset = guardedBlockStartPos.getOffset(); + add = true; + modElementFinished = false; + } else if (blockCompare.inside()) { + add = false; + modElementFinished = true; + } else { + LOG.info("Unexpected blockCompare=" + blockCompare); + add = false; + modElementFinished = true; + } + } else { + add = true; + modElementFinished = true; + } + if (add) { + try { + if (startOffset != endOffset) { + PositionRegion block = new PositionRegion(doc, startOffset, endOffset); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Reformat-at-save: add block=" + block); + } + formatBlocks.add(block); + } + } catch (BadLocationException ex) { + Exceptions.printStackTrace(ex); + } + } + startOffset = endOffset; + endOffset = modElementEndOffset; + } while (!modElementFinished); + } + + try { + for (PositionRegion block : formatBlocks) { + if (canceled.get()) { + return; + } + reformat.reformat(block.getStartOffset(), block.getEndOffset()); + } + } catch (Exception ex) { + Exceptions.printStackTrace(ex); + } + + } finally { + modRootElement.setEnabled(origEnabled); + } } } - private static final class LockInfo { - - final Reformat reformat; + private void fetchNextGuardedBlock() { + if (guardedBlockIndex < guardedBlocks.size()) { + PositionRegion guardedBlock = guardedBlocks.get(guardedBlockIndex++); + guardedBlockStartPos = guardedBlock.getStartPosition(); + guardedBlockEndPos = guardedBlock.getEndPosition(); + } else { + guardedBlockEndPos = guardedBlockStartPos = null; + } + } - final boolean modifiedLinesOnly; - - List guardedBlocks; + @MimeRegistration(mimeType="", service=OnSaveTask.Factory.class, position=500) + public static final class FactoryImpl implements Factory { - int guardedBlockIndex; - - Position guardedBlockStartPos; - - Position guardedBlockEndPos; - - - public LockInfo(Reformat reformat, boolean modifiedLinesOnly) { - this.reformat = reformat; - this.modifiedLinesOnly = modifiedLinesOnly; - } - - void reformat(Document doc) { - ModRootElement modRootElement = ModRootElement.get(doc); - if (modRootElement != null) { - boolean origEnabled = modRootElement.isEnabled(); - modRootElement.setEnabled(false); - try { - // Read all guarded blocks - guardedBlocks = new GapList(); - if (doc instanceof GuardedDocument) { - MarkBlock block = ((GuardedDocument)doc).getGuardedBlockChain().getChain(); - while (block != null) { - try { - guardedBlocks.add(new PositionRegion(doc, block.getStartOffset(), block.getEndOffset())); - } catch (BadLocationException ex) { - Exceptions.printStackTrace(ex); - } - block = block.getNext(); - } - - } - - guardedBlockIndex = 0; - fetchNextGuardedBlock(); - Element modRootOrDocElement = (modifiedLinesOnly) - ? modRootElement - : DocumentInternalUtils.customElement(doc, 0, doc.getLength()); - int modElementCount = modRootOrDocElement.getElementCount(); - List formatBlocks = new ArrayList(modElementCount); - for (int i = 0; i < modElementCount; i++) { - Element modElement = modRootOrDocElement.getElement(i); - boolean modElementFinished; - boolean add; - int startOffset = modElement.getStartOffset(); - int modElementEndOffset = modElement.getEndOffset(); - int endOffset = modElementEndOffset; - do { - if (guardedBlockStartPos != null) { - BlockCompare blockCompare = BlockCompare.get( - startOffset, - endOffset, - guardedBlockStartPos.getOffset(), - guardedBlockEndPos.getOffset()); - if (blockCompare.before()) { - add = true; - modElementFinished = true; - } else if (blockCompare.after()) { - fetchNextGuardedBlock(); - add = false; - modElementFinished = false; - } else if (blockCompare.equal()) { - fetchNextGuardedBlock(); - add = false; - modElementFinished = true; - } else if (blockCompare.overlapStart()) { - endOffset = guardedBlockStartPos.getOffset(); - add = true; - modElementFinished = true; - } else if (blockCompare.overlapEnd()) { - // Skip part covered by guarded block - endOffset = guardedBlockEndPos.getOffset(); - fetchNextGuardedBlock(); - add = false; - modElementFinished = false; - } else if (blockCompare.contains()) { - endOffset = guardedBlockStartPos.getOffset(); - add = true; - modElementFinished = false; - } else if (blockCompare.inside()) { - add = false; - modElementFinished = true; - } else { - LOG.info("Unexpected blockCompare=" + blockCompare); - add = false; - modElementFinished = true; - } - } else { - add = true; - modElementFinished = true; - } - if (add) { - try { - if (startOffset != endOffset) { - PositionRegion block = new PositionRegion(doc, startOffset, endOffset); - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Reformat-at-save: add block=" + block); - } - formatBlocks.add(block); - } - } catch (BadLocationException ex) { - Exceptions.printStackTrace(ex); - } - } - startOffset = endOffset; - endOffset = modElementEndOffset; - } while (!modElementFinished); - } - - try { - for (PositionRegion block : formatBlocks) { - reformat.reformat(block.getStartOffset(), block.getEndOffset()); - } - } catch (Exception ex) { - Exceptions.printStackTrace(ex); - } - - } finally { - modRootElement.setEnabled(origEnabled); - } - } - } - - private void fetchNextGuardedBlock() { - if (guardedBlockIndex < guardedBlocks.size()) { - PositionRegion guardedBlock = guardedBlocks.get(guardedBlockIndex++); - guardedBlockStartPos = guardedBlock.getStartPosition(); - guardedBlockEndPos = guardedBlock.getEndPosition(); - } else { - guardedBlockEndPos = guardedBlockStartPos = null; - } - } - - void unlock() { - reformat.unlock(); + @Override + public OnSaveTask createTask(Context context) { + return new ReformatBeforeSaveTask(context.getDocument()); } }