[tasklist] Suggestions Cache
Tor Norbye <[email protected]>
| Newsgroups | gmane.comp.java.netbeans.modules.tasklist.devel |
|---|---|
| Organization | Sun Microsystems, Inc |
| Message-ID | <1045288192.13661.6.camel@proto> |
I've implemented a suggestions cache, such that when you switch from one file to another (and back), the document is only scanned the first time - on subsequent returns to the file the suggestions are immediately (without a scan, and without a timer delay) listed. (And yes, document edits causes the cache entry to be flushed etc.) Anyway, this is touching a lot of sensitive code (timers, background threads, etc.) so I think of the change as a bit risky. I've manually tested it a little bit and it seems to work, but please keep your eyes open, and report any problems you see. -- Tor -----Forwarded Message----- From: [email protected] To: [email protected] Subject: CVS update: MODIFIED: html ... Date: 15 Feb 2003 05:44:58 +0000 User: tor Date: 03/02/14 21:44:58 Modified: html manifest.mf module-updates.txt Added: suggestions/src/org/netbeans/modules/tasklist/suggestions SuggestionCache.java Log: * Implement a document suggestion cache. This makes switching back and forth between files "immediate" once the suggestions have been computed in the first place. The suggestions are immediately posted. * Fix HTML Rewrite action such that it can operate on not-yet-open data objects (it opens the document before processing it.) Revision Changes Path 1.26 +1 -1 tasklist/suggestions/manifest.mf http://tasklist.netbeans.org/source/browse/tasklist/suggestions/manifest.mf.diff?r1=1.25&r2=1.26 (In the diff below, changes in quantity of whitespace are not shown.) Index: manifest.mf =================================================================== RCS file: /cvs/tasklist/suggestions/manifest.mf,v retrieving revision 1.25 retrieving revision 1.26 diff -u -b -r1.25 -r1.26 --- manifest.mf 2003/02/11 06:19:18 1.25 +++ manifest.mf 2003/02/15 05:44:55 1.26 @@ -1,4 +1,4 @@ -OpenIDE-Module-Specification-Version: 0.8.30 +OpenIDE-Module-Specification-Version: 0.8.31 OpenIDE-Module: org.netbeans.modules.tasklist.suggestions/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/tasklist/suggestions/Bundle.properties OpenIDE-Module-Implementation-Version: @BUILD_NUMBER_SUBST@ 1.6 +6 -0 tasklist/suggestions/module-updates.txt http://tasklist.netbeans.org/source/browse/tasklist/suggestions/module-updates.txt.diff?r1=1.5&r2=1.6 (In the diff below, changes in quantity of whitespace are not shown.) Index: module-updates.txt =================================================================== RCS file: /cvs/tasklist/suggestions/module-updates.txt,v retrieving revision 1.5 retrieving revision 1.6 diff -u -b -r1.5 -r1.6 --- module-updates.txt 2003/02/07 08:09:21 1.5 +++ module-updates.txt 2003/02/15 05:44:55 1.6 @@ -1,3 +1,9 @@ +Version 0.8.31: +Keep a suggestion cache, such that switching back to documents +you've already scanned immediately shows the suggestions +(unless the document has been edited programatically in the +background.) + Version 0.8.30: Scan documents in descending provider type order; makes refresh less "jittery" 1.10 +2 -0 tasklist/suggestions/src/org/netbeans/modules/tasklist/suggestions/SuggestionList.java http://tasklist.netbeans.org/source/browse/tasklist/suggestions/src/org/netbeans/modules/tasklist/suggestions/SuggestionList.java.diff?r1=1.9&r2=1.10 (In the diff below, changes in quantity of whitespace are not shown.) Index: SuggestionList.java =================================================================== RCS file: /cvs/tasklist/suggestions/src/org/netbeans/modules/tasklist/suggestions/SuggestionList.java,v retrieving revision 1.9 retrieving revision 1.10 diff -u -b -r1.9 -r1.10 --- SuggestionList.java 2003/02/05 20:08:05 1.9 +++ SuggestionList.java 2003/02/15 05:44:56 1.10 @@ -59,6 +59,8 @@ category.setType(type.getName()); category.setSType(type); category.setIcon(type.getIconImage()); + // Don't duplicate the provider field! We don't want + // SMI.stuffCache to keep category task nodes stashed... SuggestionManagerImpl manager = (SuggestionManagerImpl)SuggestionManager.getDefault(); if (manager.isExpandedType(type)) { 1.32 +221 -60 tasklist/suggestions/src/org/netbeans/modules/tasklist/suggestions/SuggestionManagerImpl.java http://tasklist.netbeans.org/source/browse/tasklist/suggestions/src/org/netbeans/modules/tasklist/suggestions/SuggestionManagerImpl.java.diff?r1=1.31&r2=1.32 (In the diff below, changes in quantity of whitespace are not shown.) Index: SuggestionManagerImpl.java =================================================================== RCS file: /cvs/tasklist/suggestions/src/org/netbeans/modules/tasklist/suggestions/SuggestionManagerImpl.java,v retrieving revision 1.31 retrieving revision 1.32 diff -u -b -r1.31 -r1.32 --- SuggestionManagerImpl.java 2003/02/11 06:19:19 1.31 +++ SuggestionManagerImpl.java 2003/02/15 05:44:56 1.32 @@ -24,6 +24,7 @@ import java.beans.PropertyChangeListener; import org.netbeans.modules.tasklist.core.TaskListView; import org.netbeans.modules.tasklist.core.TaskList; +import org.netbeans.modules.tasklist.core.TLUtils; import java.io.File; import java.util.Arrays; import java.util.Comparator; @@ -90,7 +91,12 @@ /** * Actual suggestion manager provided to clients when the Suggestions * module is running. - * + *<p> + * @todo In propertyChange(), when notified by the TopComponent.Registry + * that the set of visible top components have changed, I should figure + * out which document(s) have been closed (if any) and clear them out + * of the suggestion cache. + * <p> * @author Tor Norbye */ @@ -99,10 +105,11 @@ implements DocumentListener, CaretListener, ComponentListener, PropertyChangeListener { - public static final ErrorManager err = ErrorManager.getDefault().getInstance("org.netbeans.modules.tasklist"); // NOI18N - private final boolean stats = System.getProperty("netbeans.tasklist.stats") != null; + /** Cache tracking suggestions in recently visited files */ + private SuggestionCache cache = null; + /** Construct a new SuggestionManager instance. */ public SuggestionManagerImpl() { } @@ -286,6 +293,11 @@ } docStop(); running = false; + + // Get rid of suggestion cache + if (cache != null) { + cache.flush(); + } } } @@ -1276,8 +1288,16 @@ public void register(String type, List add, List remove) { //System.out.println("register(" + type + ", " + add + // ", " + remove + ")"); + if ((type == null) && (add != null) && (remove != null)) { + // Gotta break up the calls since we cannot handle + // both adds and removes with mixed types. Do removes, + // then adds. + register(type, null, remove, getList(), !switchingFiles); + register(type, add, null, getList(), !switchingFiles); + } else { register(type, add, remove, getList(), !switchingFiles); } + } /* XXX This method fails for the case where you have a category @@ -1299,55 +1319,98 @@ // This works because all elements in the list must have the same // (meta?) type. SuggestionType type = null; + if (typeName != null) { type = SuggestionTypes.getTypes().getType(typeName); if (type == null) { throw new IllegalArgumentException("No such SuggestionType: " + typeName); } + } - List adds = addList; - /* Group nodes with identical descriptions together. - Not very useful. Need full subtypes instead. - Commented it out for now. + /* Not yet necessary - I'm always stuffing the cache on docHidden() + // Clear SuggestionCache entry, if necessary + Suggestion first = null; + SuggestionProvider provider = null; if ((addList != null) && (addList.size() > 0)) { - adds = new ArrayList(addList.size()); - ListIterator it = addList.listIterator(); - SuggestionImpl prev = (SuggestionImpl)it.next(); - while (it.hasNext()) { - SuggestionImpl curr = (SuggestionImpl)it.next(); - if (curr.getSummary().equals(prev.getSummary())) { - LinkedList subtasks = new LinkedList(); - SuggestionImpl clone = new SuggestionImpl(); - clone.copyFrom(prev); - clone.setAction(null); - prev.setLine(null); // "category" node shouldn't have - // a line position - subtasks.add(clone); // Include self in list of children - subtasks.add(curr); - String summary = prev.getSummary(); - - // Stupid stupid stupid - while (it.hasNext()) { - curr = (SuggestionImpl)it.next(); - if (summary.equals(curr.getSummary())) { - // Yup, one more - subtasks.add(curr); - } else { - if (!it.hasNext()) { - curr = null; + first = (Suggestion)addList.get(0); + } else if ((removeList != null) && (removeList.size() > 0)) { + first = (Suggestion)removeList.get(0); + } + if ((cache != null) && (first != null)) { + provider = first.getProvider(); + if ((provider != null) && + (provider instanceof DocumentSuggestionProvider)) { + Line l = first.getLine(); + if (l != null) { + Document doc = TLUtils.getDocument(l); + if (doc != null) { + cache.remove(doc); + } + } + } + } + */ + + + // Must iterate over the list repeatedly, if it contains + // multiple types + boolean split = (type == null); + ListIterator ita = null; + ListIterator itr = null; + if (split) { + List allAdds = addList; + List allRems = removeList; + if (allAdds != null) { + ita = allAdds.listIterator(); + addList = new ArrayList(allAdds.size()); + } + if (allRems != null) { + itr = allRems.listIterator(); + removeList = new ArrayList(allRems.size()); + } } + while (true) { + + // Populate the list with the next homogeneous subset of the + // same type + if (split) { + if ((ita != null) && (ita.hasNext())) { + addList.clear(); // setSize(0); ? + type = null; + while (ita.hasNext()) { + SuggestionImpl s = (SuggestionImpl)ita.next(); + if (type == null) { + type = s.getSType(); + } else if (s.getSType() != type) { + ita.previous(); // undo advance break; } + addList.add(s); } - prev.setSubtasks(subtasks); + } else { + addList = null; } - adds.add(prev); - prev = curr; + + if ((itr != null) && (itr.hasNext())) { + removeList.clear(); + type = null; + while (itr.hasNext()) { + SuggestionImpl s = (SuggestionImpl)itr.next(); + if (type == null) { + type = s.getSType(); + } else if (s.getSType() != type) { + itr.previous(); // undo advance + break; } - if (prev != null) { - adds.add(prev); + removeList.add(s); } + } else { + removeList = null; } - */ + + if ((addList == null) && (removeList == null)) { + break; + } + } SuggestionImpl category = tasklist.getCategoryTask(type, false); @@ -1371,7 +1434,7 @@ } } } - int addnum = (adds != null) ? adds.size() : 0; + int addnum = (addList != null) ? addList.size() : 0; int remnum = (removeList != null) ? removeList.size() : 0; // Assume no stupidity like overlaps in tasks between the lists int newSize = currnum + addnum - remnum; @@ -1403,11 +1466,11 @@ tasklist.addRemove(null, leftover, false, null, null); tasklist.addRemove(leftover, null, true, category, null); } - tasklist.addRemove(adds, null, true, category, null); + tasklist.addRemove(addList, null, true, category, null); } } else { // Updating tasks within the category node - tasklist.addRemove(adds, removeList, false, category, null); + tasklist.addRemove(addList, removeList, false, category, null); } // Leave category task around? Or simply make it invisible? @@ -1422,7 +1485,7 @@ // Didn't have category nodes before and don't need to // now either... boolean append = (after != null); - tasklist.addRemove(adds, removeList, append, null, after); + tasklist.addRemove(addList, removeList, append, null, after); } else { // Had category nodes before but don't need them anymore... // remove the tasks from the top list @@ -1432,8 +1495,8 @@ null); } List leftover = category.getSubtasks(); - if (adds != null) { - tasklist.addRemove(adds, null, true, null, after); + if (addList != null) { + tasklist.addRemove(addList, null, true, null, after); } if ((leftover != null) && (leftover.size() > 0)) { tasklist.addRemove(leftover, null, true, null, after); @@ -1442,6 +1505,11 @@ tasklist.removeCategory(category, true); } } + if (!split) { + break; + } + + } } /** When true, we're in the process of switching files, so a register @@ -1600,6 +1668,16 @@ */ public void rescan(final Document document, final DataObject dataobject) { + if (cache != null) { + List suggestions = cache.lookup(document); + if (suggestions != null) { + // Get rid of tasks from list + if (suggestions.size() > 0) { + register(null, null, suggestions, getList(), true); + cache.remove(document); + } + } + } /* XXX What happens if right after scheduling the request @@ -1660,7 +1738,6 @@ } } haveShown = true; - rescan(document, dataobject); } /** @@ -1674,6 +1751,13 @@ * called directly by programs. */ public void docHidden(Document document, DataObject dataobject) { + if (runTimer == null) { + stuffCache(document, dataobject); + } else { + if (cache != null) { + cache.remove(document); + } + } List providers = getDocProviders(); ListIterator it = providers.listIterator(); while (it.hasNext()) { @@ -1686,6 +1770,47 @@ } /** + * Grab all the suggestions associated with this document/dataobject + * and push it into the suggestion cache. + */ + private void stuffCache(Document document, DataObject dataobject) { + SuggestionList tasklist = getList(); + if (tasklist.getTasks() == null) { + return; + } + Iterator it = tasklist.getTasks().iterator(); + List sgs = new ArrayList(tasklist.getTasks().size()); + while (it.hasNext()) { + SuggestionImpl s = (SuggestionImpl)it.next(); + SuggestionProvider p = s.getProvider(); + // Make sure we don't pick up category nodes here!!! + if (p instanceof DocumentSuggestionProvider) { + sgs.add(s); + } + + if (s.hasSubtasks()) { + Iterator sit = s.getSubtasks().iterator(); + while (sit.hasNext()) { + s = (SuggestionImpl)sit.next(); + p = s.getProvider(); + if (p instanceof DocumentSuggestionProvider) { + sgs.add(s); + } + } + } + } + if (cache == null) { + cache = new SuggestionCache(); + } + cache.add(document, dataobject, sgs); + + // Get rid of tasks from list + if (sgs.size() > 0) { + register(null, null, sgs, getList(), true); + } + } + + /** * The given document has been closed; stop reporting suggestions * for this document and free up associated resources. * <p> @@ -2000,23 +2125,26 @@ Node[] nodes = current.getActivatedNodes(); if ((nodes == null) || (nodes.length != 1)) { + /* if (err.isLoggable(ErrorManager.INFORMATIONAL)) { err.log( "Unexpected editor component activated nodes " + // NOI18N " contents: " + nodes); // NOI18N } + */ + return; } Node node = nodes[0]; final DataObject dao = (DataObject)node.getCookie(DataObject.class); - err.log("Considering data object " + dao); + //err.log("Considering data object " + dao); if (dao == null) { return; } if (!dao.isValid()) { - err.log("The data object is not valid!"); + //err.log("The data object is not valid!"); return; } @@ -2038,7 +2166,7 @@ final EditorCookie edit = (EditorCookie)dao.getCookie(EditorCookie.class); if (edit == null) { - err.log("No editor cookie - not doing anything"); + //err.log("No editor cookie - not doing anything"); return; } @@ -2081,7 +2209,7 @@ // discover the document */ if (doc == null) { - err.log("No document handle..."); + //err.log("No document handle..."); return; } @@ -2095,20 +2223,45 @@ dataobject = dao; + docShown(doc, dataobject); + addCaretListeners(); + // XXX Use scheduleRescan instead? (but then I have to call docShown instead of rescan; //haveShown = true; //scheduleRescan(null, false, showScanDelay); + if (cache != null) { + // TODO check scanOnShow too! (when we have scanOnOpen + // as default instead of scanOnShow as is the case now. + // The semantics of the flag need to change before we + // check it here; it's always true. Make it user selectable.) + List suggestions = cache.lookup(document); + if (suggestions != null) { + register(null, suggestions, null, getList(), true); + // TODO Consider putting the above on a runtimer - but + // a much shorter runtimer (0.1 seconds or something like + // that) such that the editor gets a chance to draw itself + // etc. + + // Also wipe out the cache items since we will replace them + // when docHidden is called, or when docEdited is called, + // etc. + cache.remove(document); + return; + } + } + if (delayed) { runTimer = new Timer(showScanDelay, new ActionListener() { public void actionPerformed(ActionEvent evt) { runTimer = null; + /* if (err.isLoggable(ErrorManager.INFORMATIONAL)) { err.log("Timer expired - time to scan " + dao); } - docShown(doc, dataobject); - addCaretListeners(); + */ + rescan(doc, dataobject); } }); runTimer.setRepeats(false); @@ -2116,8 +2269,7 @@ runTimer.start(); } else { // Do it right away - docShown(doc, dataobject); - addCaretListeners(); + rescan(doc, dataobject); } } @@ -2303,6 +2455,15 @@ String prop = ev.getPropertyName(); if(prop.equals(TopComponent.Registry.PROP_OPENED)) { componentsChanged(); + + /* Clear cache documents that are closed + if (cache != null) { + // A document may have been closed - we've gotta + // go clear the item + document = ? + cache.remove(document); + } + */ } } //public void propertyChange(PropertyChangeEvent ev) { 1.1 tasklist/suggestions/src/org/netbeans/modules/tasklist/suggestions/SuggestionCache.java http://tasklist.netbeans.org/source/browse/tasklist/suggestions/src/org/netbeans/modules/tasklist/suggestions/SuggestionCache.java?rev=1.1&content-type=text/x-cvsweb-markup Index: SuggestionCache.java =================================================================== /* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun * Microsystems, Inc. All Rights Reserved. */ package org.netbeans.modules.tasklist.suggestions; import java.util.List; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import javax.swing.event.ChangeListener; import javax.swing.event.ChangeEvent; import javax.swing.text.Document; import javax.swing.event.DocumentListener; import javax.swing.event.DocumentEvent; import org.netbeans.api.tasklist.SuggestionPerformer; import org.netbeans.api.tasklist.SuggestionManager; import org.openide.cookies.EditorCookie; import org.openide.ErrorManager; import org.openide.loaders.DataObject; import org.openide.nodes.Node; import org.netbeans.modules.tasklist.core.*; import org.openide.text.CloneableEditorSupport; /** Implements a document suggestion cache. This makes switching back and forth between files "immediate" once the suggestions have been computed in the first place. The suggestions are immediately posted (not just meaning that it's not recomputed so you get the slow refresh one by one, but there's no delay either). <p> The cache works as follows: <ul> <li> When an entry is added to the cache, it adds a listener to the document. If the document changes when the document is not showing, the cache entry is invalidated/ removed. <li> SuggestionImpl's keep a provider reference. When you swap files, all the suggestions currently showing are dumped to the cache. <li> when you open a file, in docShown we first consult the cache. If found, simply use those, otherwise call docShown as before. <li> TODO: the cache listens for document closes, and wipes out cache entries <li> PENDING: the cache is a weak listener so does not prevent garbage collection. <li> PENDING: Consider whether or not the cache should have a max size. </ul> <p> Here's how the SuggestionManager uses the cache: <ul> <li> When suggestions are modified for a document, the cache is cleared for that document. <li> When the suggestions window is hidden, the cache is flushed <li> When a file is switched: <ul> <li> the current contents are added to the cache. Some optimization should be added here such that if you don't edit the file that's open, no work is done. The items are then removed from the list. <li> the cache is consulted for the list in the existing file, and only if empty (or is scanOnShow is true) does a new scan take place. </ul> <li> When documents are closed, the cache should be notified. This is done because the cache cannot easily listen for document close events, and the suggestion manager already has a way of detecting these. </ul> <p> @todo Should I create a CacheEntry class which tracks document reference, editor supports, suggestion list, etc., instead of relying on cookies to find it? @todo What about suggestions derived from Documents which have external dependencies? For example, after changing document "foo", document Bar may get new scan errors in it - but we wouldn't notice. Perhaps DocumentSuggestionProviders can provide a hint about whether or not they expect their contents to be 100% derivable from a single document. <p> * @author Tor Norbye */ class SuggestionCache implements DocumentListener, ChangeListener { private HashMap map = null; /** Add the given document and its associated suggestions to the cache. It will automatically be removed when <ul> <li> The document is edited <li> The document is closed <li> The cache is full <li> remove() is called <li> flush() is called </ul> <p> It's okay to call add repeatedly to update - you don't have to call remove() between add() calls. <p> @param doc The document to monitor in the cache @param suggestions The list of suggestions to cache. NOTE: The list should not be null. It can be empty. Null as returned from @{link lookup} means "entry not in cache. */ public void add(Document doc, DataObject dobj, List suggestions) { if (map == null) { map = new HashMap(60); // PENDING What size to use here? } doc.removeDocumentListener(this); // prevent double registering doc.addDocumentListener(this); /* This was an attempt at registering a listener such that if a document we're not paying attention to is closed, its cache entry is removed. But this doesn't work - JavaEditor for example (the EditorCookie for java data objects) is not a CloneableEditorSupport, it's just an EditorSupport. The other choices are to either use some kind of weak listener, such that the items are removed automatically by the garbage collector, or to use another way to detect when tabs are closed. Turns out we're already doing that - in the Suggestion Manager, so we'll use that scheme instead. The manager is responsible for clearing out "background" documents. If you fix this, be sure to remove the listener too, in the remove() method. EditorCookie editor = (EditorCookie)dobj.getCookie (EditorCookie.class); if (editor != null && (editor instanceof CloneableEditorSupport)) { CloneableEditorSupport supp = (CloneableEditorSupport)editor; if (supp != null) { supp.removeChangeListener(this); supp.addChangeListener(this); } } */ map.put(doc, suggestions); } /** Look up the given document's suggestions list. * @param doc The document to look up suggestions for * @return The list of suggestions associated with this * document. Will return null if the document has * not been cached, or if it has been flushed from the * cache. */ public List lookup(Document doc) { if (map == null) { return null; } return (List)map.get(doc); } /** Remove all items from the cache */ public void flush() { if (map == null) { return; } map.clear(); } /** Remove a particular document's stashed list in the cache. @param doc The document we want removed from the cache */ public void remove(Document doc) { if (map.remove(doc) != null) { // Was in the table - gotta remove doc listeners doc.removeDocumentListener(this); // If you fix auto-listening for document closure // (see section around addDocumentListener further up), // make sure you remove the changeListener too! //supp.removeChangeListener(this); } } /** The given document should be removed from the map */ private void invalidate(Document doc) { remove(doc); } // Implements DocumentListener public void changedUpdate(DocumentEvent e) { // Don't care - attribute changes only } public void insertUpdate(DocumentEvent e) { invalidate(e.getDocument()); } public void removeUpdate(DocumentEvent e) { invalidate(e.getDocument()); } public void stateChanged(ChangeEvent evt) { // Remove listener too, if invalidate fails CloneableEditorSupport supp = (CloneableEditorSupport)evt.getSource(); invalidate(supp.getDocument()); } } 1.6 +1 -1 tasklist/html/manifest.mf http://tasklist.netbeans.org/source/browse/tasklist/html/manifest.mf.diff?r1=1.5&r2=1.6 (In the diff below, changes in quantity of whitespace are not shown.) Index: manifest.mf =================================================================== RCS file: /cvs/tasklist/html/manifest.mf,v retrieving revision 1.5 retrieving revision 1.6 diff -u -b -r1.5 -r1.6 --- manifest.mf 2003/02/12 01:56:25 1.5 +++ manifest.mf 2003/02/15 05:44:57 1.6 @@ -1,4 +1,4 @@ -OpenIDE-Module-Specification-Version: 0.8.30 +OpenIDE-Module-Specification-Version: 0.8.31 OpenIDE-Module: org.netbeans.modules.tasklist.html/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/tasklist/html/Bundle.properties OpenIDE-Module-Layer: org/netbeans/modules/tasklist/html/mf-layer.xml 1.2 +6 -0 tasklist/html/module-updates.txt http://tasklist.netbeans.org/source/browse/tasklist/html/module-updates.txt.diff?r1=1.1&r2=1.2 (In the diff below, changes in quantity of whitespace are not shown.) Index: module-updates.txt =================================================================== RCS file: /cvs/tasklist/html/module-updates.txt,v retrieving revision 1.1 retrieving revision 1.2 diff -u -b -r1.1 -r1.2 --- module-updates.txt 2003/01/30 18:52:12 1.1 +++ module-updates.txt 2003/02/15 05:44:57 1.2 @@ -1,3 +1,9 @@ +Version 0.8.31 +Fix HTML Rewrite action such that it can operate on not-yet-open +data objects (it opens the document before processing it.) +Also change action name to something a little bit more meaningful: +JTidy: Clean up & Format... + Version 0.2 First version. Provides html error messages for html documents (error scanning by "tidy") --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected] -- Tor Norbye <[email protected]> Sun Microsystems, Inc