[572227] Add an option for headless text output without a zip

Option to use the new text renderer for UI export.
Add tree expansion to text renderer.

Change-Id: Ie6c11566c95675f1f1f421a87ec57a8eb9b8041b
Task-Url: https://bugs.eclipse.org/bugs/show_bug.cgi?id=572227
diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java
index 7c288c2..3192829 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java
@@ -1,10 +1,10 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2020 SAP AG, IBM Corporation and others.
+ * Copyright (c) 2008, 2021 SAP AG, IBM Corporation and others.

  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License 2.0

  * which accompanies this distribution, and is available at

- * https://www.eclipse.org/legal/epl-2.0/
- *
+ * https://www.eclipse.org/legal/epl-2.0/

+ *

  * SPDX-License-Identifier: EPL-2.0

  *

  * Contributors:

@@ -255,7 +255,7 @@
         {

             overview.append("<p>"); //$NON-NLS-1$

             overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_Thread, //

-                            HTMLUtils.escapeText(suspect.getSuspect().getDisplayName()), //
+                            HTMLUtils.escapeText(suspect.getSuspect().getDisplayName()), //

                             formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));

             overview.append("</p>"); //$NON-NLS-1$

         }

@@ -282,7 +282,7 @@
             String classloaderName = getClassLoaderName(suspectClassloader, keywords);

 

             overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_Class, //

-                            HTMLUtils.escapeText(className), classloaderName, formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));
+                            HTMLUtils.escapeText(className), classloaderName, formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));

         }

         else

         {

@@ -297,7 +297,7 @@
             String classloaderName = getClassLoaderName(suspectClassloader, keywords);

 

             overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_Instance, //

-                            HTMLUtils.escapeText(className), classloaderName, formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));
+                            HTMLUtils.escapeText(className), classloaderName, formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));

 

             /*

              * if the class name matches the skip pattern, try to find the first

@@ -338,8 +338,8 @@
 //                        involvedClassloaders.add(suspectClassloader);

                         objectsForTroubleTicketInfo.add(referrer);

                         String referrerClassloaderName = getClassLoaderName(referrerClassloader, keywords);

-                        overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_ReferencedByInstance, HTMLUtils.escapeText(referrer
-                                        .getDisplayName()), referrerClassloaderName));
+                        overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_ReferencedByInstance, HTMLUtils.escapeText(referrer

+                                        .getDisplayName()), referrerClassloaderName));

 

                     }

                 }

@@ -371,7 +371,7 @@
 

                 String classloaderName = getClassLoaderName(accPointClassloader, keywords);

 

-                overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedByLoadedBy, HTMLUtils.escapeText(clazz.getName()),
+                overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedByLoadedBy, HTMLUtils.escapeText(clazz.getName()),

                                 classloaderName, formatRetainedHeap(suspect.getAccumulationPoint().getRetainedHeapSize(), totalHeap)));

             }

             else

@@ -385,7 +385,7 @@
                 objectsForTroubleTicketInfo.add(accumulationObject);

 

                 String classloaderName = getClassLoaderName(accPointClassloader, keywords);

-                overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedByInstance, HTMLUtils.escapeText(className),
+                overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedByInstance, HTMLUtils.escapeText(className),

                                 classloaderName, formatRetainedHeap(suspect.getAccumulationPoint().getRetainedHeapSize(), totalHeap)));

             }

         }

@@ -508,7 +508,7 @@
         String classloaderName = getClassLoaderName(classloader, keywords);

 

         String numberOfInstances = numberFormatter.format(suspect.getSuspectInstances().length);

-        builder.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_InstancesOccupy, numberOfInstances, HTMLUtils.escapeText(className),
+        builder.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_InstancesOccupy, numberOfInstances, HTMLUtils.escapeText(className),

                         classloaderName, formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));

 

         int[] suspectInstances = suspect.getSuspectInstances();

@@ -527,13 +527,13 @@
             builder.append("<ul>"); //$NON-NLS-1$

             for (IObject inst : bigSuspectInstances)

             {

-                builder.append("<li>").append(HTMLUtils.escapeText(inst.getDisplayName())); //$NON-NLS-1$
+                builder.append("<li>").append(HTMLUtils.escapeText(inst.getDisplayName())); //$NON-NLS-1$

                 builder.append("&nbsp;-&nbsp;") //$NON-NLS-1$

                                 .append(

                                                 MessageUtil.format(Messages.LeakHunterQuery_Msg_Bytes,

                                                                 formatRetainedHeap(inst.getRetainedHeapSize(),

                                                                                 totalHeap)));

-                builder.append("</li>"); //$NON-NLS-1$
+                builder.append("</li>"); //$NON-NLS-1$

             }

             builder.append("</ul>"); //$NON-NLS-1$

         }

@@ -831,12 +831,12 @@
         return name;

     }

 

-    /**
-     * Get the name of the class loader.
-     * @param classloader
-     * @param keywords
-     * @return The name with HTML escapes already applied.
-     */
+    /**

+     * Get the name of the class loader.

+     * @param classloader

+     * @param keywords

+     * @return The name with HTML escapes already applied.

+     */

     private String getClassLoaderName(IObject classloader, Set<String> keywords)

     {

         if (classloader.getObjectAddress() == 0)

@@ -850,7 +850,7 @@
             {

                 keywords.add(classloaderName);

             }

-            return HTMLUtils.escapeText(classloaderName);
+            return HTMLUtils.escapeText(classloaderName);

         }

     }

 

@@ -948,7 +948,7 @@
     {

         builder.append("<b>").append(Messages.LeakHunterQuery_Keywords).append("</b><br>"); //$NON-NLS-1$ //$NON-NLS-2$

         for (String s : keywords)

-            builder.append(HTMLUtils.escapeText(s)).append("<br>"); //$NON-NLS-1$
+            builder.append(HTMLUtils.escapeText(s)).append("<br>"); //$NON-NLS-1$

     }

 

     private void appendTroubleTicketInformation(List<IObject> classloaders, StringBuilder builder)

@@ -960,12 +960,12 @@
 

             if (!mapping.isEmpty())

             {

-                builder.append("<br><b>").append(HTMLUtils.escapeText(resolver.getTicketSystem())).append("</b><br>"); //$NON-NLS-1$ //$NON-NLS-2$
+                builder.append("<br><b>").append(HTMLUtils.escapeText(resolver.getTicketSystem())).append("</b><br>"); //$NON-NLS-1$ //$NON-NLS-2$

                 for (Map.Entry<String, String> entry : mapping.entrySet())

                 {

                     builder.append(

-                                    MessageUtil.format(Messages.LeakHunterQuery_TicketForSuspect, HTMLUtils.escapeText(entry.getKey()), HTMLUtils.escapeText(entry
-                                                    .getValue()))).append("<br>"); //$NON-NLS-1$
+                                    MessageUtil.format(Messages.LeakHunterQuery_TicketForSuspect, HTMLUtils.escapeText(entry.getKey()), HTMLUtils.escapeText(entry

+                                                    .getValue()))).append("<br>"); //$NON-NLS-1$

                 }

             }

         }

@@ -993,7 +993,7 @@
                 builder.append("<p>"); //$NON-NLS-1$

 

                 for (CompositeResult.Entry requestInfo : requestInfos.getResultEntries())

-                    builder.append(HTMLUtils.escapeText(requestInfo.getName())).append(" ").append( //$NON-NLS-1$
+                    builder.append(HTMLUtils.escapeText(requestInfo.getName())).append(" ").append( //$NON-NLS-1$

                                                     textResult.linkTo(Messages.LeakHunterQuery_RequestDetails,

                                                                     requestInfo.getResult())).append("<br>"); //$NON-NLS-1$

 

@@ -1090,14 +1090,26 @@
                          */

                         public boolean isExpanded(Object row)

                         {

+                            // Any thread should be expanded

                             if (rt.getElements().contains(row))

                                 return true;

                             for (Object r2 : rt.getChildren(row))

                             {

-                                // if row is a stack frame row

+                                // If row has a child which is a local

                                 IContextObject co = rt.getContext(r2);

                                 if (co != null && locals.contains(co.getObjectId()))

-                                { return true; }

+                                {

+                                    /*

+                                     * It needs to be a stack frame row though to

+                                     * be expanded.

+                                     */

+                                    for (Object r3 : rt.getElements())

+                                    {

+                                        // Relies on ThreadOverviewQuery.ThreadStackFrameNode equals()

+                                        if (rt.getChildren(r3).contains(row))

+                                            return true;

+                                    }

+                                }

                             }

                             return false;

                         }

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/threads/ThreadOverviewQuery.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/threads/ThreadOverviewQuery.java
index edc1dab..1142b18 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/threads/ThreadOverviewQuery.java
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/threads/ThreadOverviewQuery.java
@@ -1,10 +1,10 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2020 SAP AG, IBM Corporation and others.

+ * Copyright (c) 2008, 2021 SAP AG, IBM Corporation and others.

  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License 2.0

  * which accompanies this distribution, and is available at

- * https://www.eclipse.org/legal/epl-2.0/
- *
+ * https://www.eclipse.org/legal/epl-2.0/

+ *

  * SPDX-License-Identifier: EPL-2.0

  *

  * Contributors:

@@ -238,6 +238,28 @@
     

     private static class ThreadStackFrameNode

     {

+        @Override

+        public int hashCode()

+        {

+            final int prime = 31;

+            int result = 1;

+            result = prime * result + depth;

+            return result;

+        }

+        @Override

+        public boolean equals(Object obj)

+        {

+            if (this == obj)

+                return true;

+            if (obj == null)

+                return false;

+            if (getClass() != obj.getClass())

+                return false;

+            ThreadStackFrameNode other = (ThreadStackFrameNode) obj;

+            if (depth != other.depth)

+                return false;

+            return true;

+        }

         private ThreadOverviewNode threadOverviewNode;

         private IStackFrame stackFrame;

         private int depth;

diff --git a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/CSVOutputter.java b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/CSVOutputter.java
index 3d0cfa2..3ce39c5 100644
--- a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/CSVOutputter.java
+++ b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/CSVOutputter.java
@@ -22,8 +22,6 @@
 import org.eclipse.mat.query.IResultTable;

 import org.eclipse.mat.query.IResultTree;

 import org.eclipse.mat.query.refined.Filter;

-import org.eclipse.mat.query.refined.RefinedTable;

-import org.eclipse.mat.query.refined.RefinedTree;

 import org.eclipse.mat.report.IOutputter;

 import org.eclipse.mat.report.Renderer;

 

@@ -42,8 +40,8 @@
     public void embedd(Context context, IResult result, Writer writer) throws IOException

     {

         // add column names to first row

-        Column[] columns = (result instanceof RefinedTable) ? ((RefinedTable) result).getColumns()

-                        : ((RefinedTree) result).getColumns();

+        Column[] columns = (result instanceof IResultTable) ? ((IResultTable) result).getColumns()

+                        : ((IResultTree) result).getColumns();

         Filter.ValueConverter[] filter = new Filter.ValueConverter[columns.length];

 

         for (int columnIndex = 0; columnIndex < columns.length; columnIndex++)

@@ -61,9 +59,9 @@
         writer.append("\n"); //$NON-NLS-1$

 

         // add data records

-        if (result instanceof RefinedTable)

+        if (result instanceof IResultTable)

         {

-            RefinedTable table = ((RefinedTable) result);

+            IResultTable table = ((IResultTable) result);

             int limit = context.hasLimit() ? Math.min(table.getRowCount(), context.getLimit()) : table.getRowCount();

 

             for (int row = 0; row < limit; row++)

@@ -82,10 +80,10 @@
                 writer.append("\n"); //$NON-NLS-1$

             }

         }

-        else if (result instanceof RefinedTree)

+        else if (result instanceof IResultTree)

         {

             // export only first level of the RefinedTree

-            RefinedTree tree = (RefinedTree) result;

+            IResultTree tree = (IResultTree) result;

             List<?> elements = tree.getElements();

             int limit = context.hasLimit() ? Math.min(elements.size(), context.getLimit()) : elements.size();

 

diff --git a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/HtmlOutputter.java b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/HtmlOutputter.java
index d2429d3..79f93f2 100644
--- a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/HtmlOutputter.java
+++ b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/HtmlOutputter.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2019 SAP AG and IBM Corporation.

+ * Copyright (c) 2008, 2021 SAP AG and IBM Corporation.

  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License 2.0

  * which accompanies this distribution, and is available at

@@ -242,10 +242,19 @@
                     }

                     else

                     {

-                        if (columns[i].isNumeric())

-                            artefact.append("<td align=\"right\">");

-                        else

-                            artefact.append("<td>");

+                        switch (columns[i].getAlign())

+                        {

+                            case RIGHT:

+                                artefact.append("<td align=\"right\">");

+                                break;

+                            case CENTER:

+                                artefact.append("<td align=\"center\">");

+                                break;

+                            case LEFT:

+                            default:

+                                artefact.append("<td>");

+                                break;

+                        }

                         artefact.append(totalsRow.getLabel(i)).append("</td>");

                     }

                 }

@@ -395,10 +404,19 @@
         {

             if (context.isColumnVisible(columnIndex))

             {

-                if (columns[columnIndex].isNumeric())

-                    artefact.append("<td align=\"right\">");

-                else

-                    artefact.append("<td>");

+                switch (columns[columnIndex].getAlign())

+                {

+                    case RIGHT:

+                        artefact.append("<td align=\"center\">");

+                        break;

+                    case CENTER:

+                        artefact.append("<td align=\"center\">");

+                        break;

+                    case LEFT:

+                    default:

+                        artefact.append("<td>");

+                        break;

+                }

 

                 renderColumnValue(context, artefact, structured, columns, row, columnIndex);

                 artefact.append("</td>");

diff --git a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/RenderingInfo.java b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/RenderingInfo.java
index 3e2c6e8..4cfc15e 100644
--- a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/RenderingInfo.java
+++ b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/RenderingInfo.java
@@ -1,14 +1,15 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2010 SAP AG.

+ * Copyright (c) 2008, 2021 SAP AG and IBM Corporation.

  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License 2.0

  * which accompanies this distribution, and is available at

- * https://www.eclipse.org/legal/epl-2.0/
- *
+ * https://www.eclipse.org/legal/epl-2.0/

+ *

  * SPDX-License-Identifier: EPL-2.0

  *

  * Contributors:

  *    SAP AG - initial API and implementation

+ *    Andrew Johnson - bug fixes for different renderers

  *******************************************************************************/

 

 package org.eclipse.mat.report.internal;

@@ -19,7 +20,9 @@
 import org.eclipse.mat.query.IQueryContext;

 import org.eclipse.mat.query.IResult;

 import org.eclipse.mat.report.IOutputter;

+import org.eclipse.mat.report.Params;

 import org.eclipse.mat.report.QuerySpec;

+import org.eclipse.mat.report.RendererRegistry;

 import org.eclipse.mat.report.Spec;

 

 /* package */class RenderingInfo implements IOutputter.Context

@@ -89,7 +92,21 @@
 

         if (filename == null)

         {

-            filename = ResultRenderer.DIR_PAGES + '/' + child.getId() + ".html"; //$NON-NLS-1$

+            /*

+             * Guess the format that might be used.

+             * For example,  "stacktrace with involved local variables"

+             * in leak suspects TextResult as HTML with a default

+             * mode of csv.

+             * The file name is decided here, but the true format is

+             * decided later.

+             */

+            String format = child.params().get(Params.FORMAT, "html"); //$NON-NLS-1$

+            IOutputter outputter = RendererRegistry.instance().match(format, result.getClass());

+            if (outputter == null && result instanceof QuerySpec)

+                outputter = RendererRegistry.instance().match(format, ((QuerySpec)result).getResult().getClass());

+            if (outputter == null)

+                format = "html"; //$NON-NLS-1$

+            filename = ResultRenderer.DIR_PAGES + '/' + child.getId() + "." + format; //$NON-NLS-1$

             dataFile.setSuggestedFile(filename);

         }

 

diff --git a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/TextEmitter.java b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/TextEmitter.java
index b9b5802..7e38da6 100644
--- a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/TextEmitter.java
+++ b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/TextEmitter.java
@@ -104,7 +104,7 @@
             }

 

             // add column names to the result buffer

-            for (int i = 0; i < numberOfColumns; i++)

+            for (int i = 0; i < order.length; i++)

             {

                 int col = order[i];

                 if (i != 0)

@@ -120,7 +120,7 @@
             for (Object item : items)

             {

                 boolean addLineBreak = true;

-                for (int i = 0; i < numberOfColumns; i++)

+                for (int i = 0; i < order.length; i++)

                 {

                     int columnIndex = order[i];

                     if (shouldSuppressLineBreak(item))

@@ -207,7 +207,7 @@
                         append(align(getDisplayableColumnValue(children[j], col), align[col], length - level.length(),

                                         numberOfColumns == 1));

                         // add the rest of the columns

-                        for (int i = 1; i < numberOfColumns; i++)

+                        for (int i = 1; i < order.length; i++)

                         {

                             col = order[i];

                             append(COLUMN_SEPARATOR);

@@ -324,7 +324,7 @@
     {

         StringBuilder dashes = new StringBuilder();

         int dashesLength = 0;

-        for (int i = 0; i < numberOfColumns; i++)

+        for (int i = 0; i < order.length; i++)

         {

             int col = order[i];

             // column separator

diff --git a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/TextOutputter.java b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/TextOutputter.java
index 2eb70f0..47d9632 100644
--- a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/TextOutputter.java
+++ b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/TextOutputter.java
@@ -14,19 +14,23 @@
 

 import java.io.IOException;

 import java.io.Writer;

+import java.util.Arrays;

 import java.util.List;

 

 import org.eclipse.mat.query.Column;

 import org.eclipse.mat.query.Column.Alignment;

+import org.eclipse.mat.query.IDecorator;

 import org.eclipse.mat.query.IResult;

 import org.eclipse.mat.query.IResultTable;

 import org.eclipse.mat.query.IResultTree;

+import org.eclipse.mat.query.ISelectionProvider;

 import org.eclipse.mat.query.refined.Filter;

-import org.eclipse.mat.query.refined.RefinedTable;

-import org.eclipse.mat.query.refined.RefinedTree;

+import org.eclipse.mat.query.refined.RefinedStructuredResult;

+import org.eclipse.mat.query.refined.TotalsRow;

 import org.eclipse.mat.query.results.TextResult;

 import org.eclipse.mat.report.IOutputter;

 import org.eclipse.mat.report.Renderer;

+import org.eclipse.mat.util.VoidProgressListener;

 

 @Renderer(target = "txt", result = { IResultTree.class, IResultTable.class, TextResult.class })

 public class TextOutputter extends OutputterBase implements IOutputter

@@ -45,11 +49,11 @@
             writer.append(((TextResult) result).getText());

             writer.append(LINE_SEPARATOR);

         }

-        else if (result instanceof RefinedTable)

+        else if (result instanceof IResultTable)

         {

             new RefinedTableTextEmitter(context, result, writer).doCopy();

         }

-        else if (result instanceof RefinedTree)

+        else if (result instanceof IResultTree)

         {

             new RefinedTreeTextEmitter(context, result, writer).doCopy();

         }

@@ -111,10 +115,17 @@
             order = new int[columns.length];

             filter = new Filter.ValueConverter[columns.length];

 

+            int orderIndex = 0;

             for (int columnIndex = 0; columnIndex < columns.length; columnIndex++)

             {

                 filter[columnIndex] = (Filter.ValueConverter) columns[columnIndex].getData(Filter.ValueConverter.class);

-                order[columnIndex] = columnIndex;

+                /*

+                 * It's a bit complicated to handle invisible columns

+                 * because the columnIndex may be used to lookup values.

+                 * Omit the column from the ordering.

+                 */

+                if (context.isColumnVisible(columnIndex))

+                    order[orderIndex++] = columnIndex;

                 Alignment alignment = columns[columnIndex].getAlign();

                 switch (alignment)

                 {

@@ -128,13 +139,9 @@
                         align[columnIndex] = ALIGN_LEFT;

                         break;

                 }

-

-                // TODO it's a bit complicated to handle invisible columns

-                // because the columnIndex may be used to lookup values

-                // in the underlying IResult, so we would need to add a mapping

-                //

-                // if (context.isColumnVisible(columnIndex))

             }

+            if (orderIndex < order.length)

+                order = Arrays.copyOf(order, orderIndex);

         }

 

         @Override

@@ -197,7 +204,7 @@
 

     private static class RefinedTableTextEmitter extends StructuredResultTextEmitter

     {

-        private RefinedTable table;

+        private IResultTable table;

 

         public RefinedTableTextEmitter(Context context, IResult result, Writer writer)

         {

@@ -207,17 +214,38 @@
         @Override

         protected void initialize(IResult result)

         {

-            this.table = (RefinedTable) result;

+            this.table = (IResultTable) result;

         }

 

         @Override

         protected Object[] getItems()

         {

             int rows = context.hasLimit() ? Math.min(table.getRowCount(), context.getLimit()) : table.getRowCount();

-            Object[] result = new Object[rows];

+            int rows1 = context.isTotalsRowVisible() && table instanceof RefinedStructuredResult ? rows + 1 : rows;

+            int rows0 = 0;

+            if (table instanceof RefinedStructuredResult && ((RefinedStructuredResult) table).hasActiveFilter())

+            {

+                ++rows1;

+                rows0 = 1;

+            }

+            Object[] result = new Object[rows1];

             for (int i = 0; i < rows; i++)

             {

-                result[i] = table.getRow(i);

+                result[rows0 + i] = table.getRow(i);

+            }

+            if (table instanceof RefinedStructuredResult)

+            {

+                RefinedStructuredResult rsr = (RefinedStructuredResult)table;

+                if (rows0 == 1)

+                    result[0] = rsr.getFilter();

+                if (rows1 > rows0 + rows)

+                {

+                    List<Object> subList = Arrays.asList(result).subList(rows0, rows0 + rows);

+                    final TotalsRow totalsRow = rsr.buildTotalsRow(subList);

+                    totalsRow.setVisibleItems(rows);

+                    rsr.calculateTotals(subList, totalsRow, new VoidProgressListener());

+                    result[rows1 - 1] = totalsRow;

+                }

             }

             return result;

         }

@@ -231,25 +259,73 @@
         @Override

         protected String getItemValue(Object item, int columnIndex)

         {

+            if (item instanceof Filter[])

+            {

+                Filter f[] = (Filter[])item;

+                String fv = f[columnIndex].getCriteria();

+                if (fv == null)

+                    fv = ""; //$NON-NLS-1$

+                return fv;

+            }

+            if (item instanceof TotalsRow)

+            {

+                TotalsRow tr = (TotalsRow)item;

+                return tr.getLabel(columnIndex);

+            }

+            if (table instanceof RefinedStructuredResult)

+                return ((RefinedStructuredResult)table).getFormattedColumnValue(item, columnIndex);

             return getStringValue(table.getColumnValue(item, columnIndex), filter[columnIndex]);

         }

 

         @Override

         protected String getDisplayableColumnValue(Object item, int index)

         {

+            if (item instanceof Filter[])

+            {

+                Filter f[] = (Filter[])item;

+                String fv = f[index].getCriteria();

+                if (fv == null)

+                    fv = ""; //$NON-NLS-1$

+                return fv;

+            }

+            if (item instanceof TotalsRow)

+            {

+                TotalsRow tr = (TotalsRow)item;

+                return tr.getLabel(index);

+            }

+            if (table instanceof RefinedStructuredResult)

+            {

+                String v = ((RefinedStructuredResult)table).getFormattedColumnValue(item, index);

+                IDecorator dec = table.getColumns()[index].getDecorator();

+                if (dec != null)

+                {

+                    String prefix = dec.prefix(item);

+                    String suffix = dec.suffix(item);

+                    if (prefix != null)

+                        if (suffix != null)

+                            v = prefix + " " + v + " " + suffix; //$NON-NLS-1$ //$NON-NLS-2$

+                        else

+                            v = prefix + " " + v; //$NON-NLS-1$

+                    else

+                        if (suffix != null)

+                            v = v + " " + suffix; //$NON-NLS-1$

+                }

+                return v;

+            }

             return getStringValue(table.getColumnValue(item, index), filter[index]);

         }

 

         @Override

         protected boolean isExpanded(Object item)

         {

-            return table.isExpanded(item);

+            return false;

         }

     }

 

     private static class RefinedTreeTextEmitter extends StructuredResultTextEmitter

     {

-        private RefinedTree tree;

+        private IResultTree tree;

+        private ISelectionProvider sel;

 

         public RefinedTreeTextEmitter(Context context, IResult result, Writer writer)

         {

@@ -259,18 +335,48 @@
         @Override

         protected void initialize(IResult result)

         {

-            tree = (RefinedTree) result;

+            tree = (IResultTree) result;

+            if (result instanceof ISelectionProvider)

+                sel  = (ISelectionProvider)result;

+            else

+                sel = ISelectionProvider.EMPTY;

         }

 

         @Override

         protected Object[] getItems()

         {

             List<?> elements = tree.getElements();

+            return getItems(elements);

+        }

+

+        protected Object[] getItems(List<?> elements)

+        {

             int rows = context.hasLimit() ? Math.min(elements.size(), context.getLimit()) : elements.size();

-            Object[] result = new Object[rows];

+            int rows1 = context.isTotalsRowVisible() && tree instanceof RefinedStructuredResult ? rows + 1 : rows;

+            int rows0 = 0;

+            if (tree instanceof RefinedStructuredResult && ((RefinedStructuredResult) tree).hasActiveFilter())

+            {

+                ++rows1;

+                rows0 = 1;

+            }

+            Object[] result = new Object[rows1];

             for (int i = 0; i < rows; i++)

             {

-                result[i] = elements.get(i);

+                result[rows0 + i] = elements.get(i);

+            }

+            if (tree instanceof RefinedStructuredResult)

+            {

+                RefinedStructuredResult rsr = (RefinedStructuredResult)tree;

+                if (rows0 == 1)

+                    result[0] = rsr.getFilter();

+                if (rows1 > rows0 + rows)

+                {

+                    List<Object> subList = Arrays.asList(result).subList(rows0, rows0 + rows);

+                    final TotalsRow totalsRow = rsr.buildTotalsRow(subList);

+                    totalsRow.setVisibleItems(rows);

+                    rsr.calculateTotals(subList, totalsRow, new VoidProgressListener());

+                    result[rows1 - 1] = totalsRow;

+                }

             }

             return result;

         }

@@ -284,19 +390,185 @@
         @Override

         protected String getItemValue(Object item, int columnIndex)

         {

+            if (item instanceof Filter[])

+            {

+                Filter f[] = (Filter[])item;

+                String fv = f[columnIndex].getCriteria();

+                if (fv == null)

+                    fv = ""; //$NON-NLS-1$

+                return fv;

+            }

+            if (item instanceof TotalsRow)

+            {

+                TotalsRow tr = (TotalsRow)item;

+                return tr.getLabel(columnIndex);

+            }

+            if (tree instanceof RefinedStructuredResult)

+                return ((RefinedStructuredResult)tree).getFormattedColumnValue(item, columnIndex);

             return getStringValue(tree.getColumnValue(item, columnIndex), filter[columnIndex]);

         }

 

         @Override

         protected String getDisplayableColumnValue(Object item, int index)

         {

+            if (item instanceof Filter[])

+            {

+                Filter f[] = (Filter[])item;

+                String fv = f[index].getCriteria();

+                if (fv == null)

+                    fv = ""; //$NON-NLS-1$

+                return fv;

+            }

+            if (item instanceof TotalsRow)

+            {

+                TotalsRow tr = (TotalsRow)item;

+                return tr.getLabel(index);

+            }

+            if (tree instanceof RefinedStructuredResult)

+            {

+                String v = ((RefinedStructuredResult)tree).getFormattedColumnValue(item, index);

+                IDecorator dec = tree.getColumns()[index].getDecorator();

+                if (dec != null)

+                {

+                    String prefix = dec.prefix(item);

+                    String suffix = dec.suffix(item);

+                    if (prefix != null)

+                        if (suffix != null)

+                            v = prefix + " " + v + " " + suffix; //$NON-NLS-1$ //$NON-NLS-2$

+                        else

+                            v = prefix + " " + v; //$NON-NLS-1$

+                    else

+                        if (suffix != null)

+                            v = v + " " + suffix; //$NON-NLS-1$

+                }

+                return v;

+            }

             return getStringValue(tree.getColumnValue(item, index), filter[index]);

         }

 

         @Override

         protected boolean isExpanded(Object item)

         {

-            return tree.isExpanded(item);

+            if (item instanceof TotalsRow)

+                return false;

+            return sel.isExpanded(item);

         }

+

+        @Override

+        protected boolean shouldAddNextLine(Object item)

+        {

+            return isExpanded(item) && toPrint(item);

+        }

+

+        private boolean toPrint(Object item)

+        {

+            return true;

+        }

+

+        @Override

+        protected boolean shouldProcessChild(Object child)

+        {

+            return true;

+        }

+

+        @Override

+        protected Object[] getChildren(Object item)

+        {

+            if (item instanceof TotalsRow)

+                return null;

+            if (tree.hasChildren(item))

+            {

+                List<?> children = tree.getChildren(item);

+                if (children != null)

+                    return getItems(children);

+            }

+            return null;

+        }

+        

+        protected int getColumnLength(Object[] items, Object[] objColumns, int columnNumber)

+        {

+            int lengthToCompare = 0;

+            String header = getColumnName(objColumns[columnNumber]);

+            int length = header != null ? header.length() : 0;

+

+            for (int i = 0; i < items.length; i++)

+            {

+                lengthToCompare = getDisplayableColumnValue(items[i], columnNumber).length();

+

+                if (lengthToCompare > length)

+                    length = lengthToCompare;

+

+                if (isExpanded(items[i]))

+                {

+                    if (columnNumber == order[0])

+                        lengthToCompare = compare(items[i], length, columnNumber, new StringBuilder());

+                    else

+                        lengthToCompare = getOtherColumnLength(items[i], length, columnNumber);

+

+                }

+                if (lengthToCompare > length)

+                    length = lengthToCompare;

+            }

+

+            return length;

+        }

+

+        private int getOtherColumnLength(Object item, int length, int columnNumber)

+        {

+            int lengthToCompare = 0;

+            Object[] children = getChildren(item);

+            if (children != null)

+            {

+                for (int i = 0; i < children.length; i++)

+                {

+                    //if (selection != null && skip(children[i]))

+                    //    continue;

+                    String columnText = getDisplayableColumnValue(children[i], columnNumber);

+                    if (columnText != null)

+                        lengthToCompare = columnText.length();

+                    if (lengthToCompare > length)

+                        length = lengthToCompare;

+

+                    if (isExpanded(children[i]))

+                    {

+                        lengthToCompare = getOtherColumnLength(children[i], length, columnNumber);

+                        if (lengthToCompare > length)

+                            length = lengthToCompare;

+                    }

+

+                }

+            }

+            return length;

+        }

+

+        private int compare(Object item, int length, int columnNumber, StringBuilder level)

+        {

+            int lengthToCompare = 0;

+            Object[] children = getChildren(item);

+            if (children != null)

+            {

+                for (int i = 0; i < children.length; i++)

+                {

+                    //if (selection != null && skip(children[i]))

+                    //    continue;

+                    level = getLevel(level, children.length, i);

+                    lengthToCompare = getDisplayableColumnValue(children[i], columnNumber).length() + level.length();

+                    if (lengthToCompare > length)

+                        length = lengthToCompare;

+

+                    if (isExpanded(children[i]))

+                    {

+                        lengthToCompare = compare(children[i], length, columnNumber, level);

+                        if (lengthToCompare > length)

+                            length = lengthToCompare;

+                    }

+                    if (level.length() >= 3)

+                        level.delete(level.length() - 3, level.length());

+

+                }

+            }

+            return length;

+        }

+    

     }

 }

diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/GeneralSnapshotTests.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/GeneralSnapshotTests.java
index 8be535d..25f9a1b 100644
--- a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/GeneralSnapshotTests.java
+++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/GeneralSnapshotTests.java
@@ -25,7 +25,9 @@
 import static org.hamcrest.collection.IsEmptyCollection.emptyCollectionOf;

 import static org.hamcrest.core.IsInstanceOf.instanceOf;

 import static org.hamcrest.core.IsNull.nullValue;

+import static org.hamcrest.number.OrderingComparison.lessThanOrEqualTo;

 import static org.junit.Assert.assertEquals;

+import static org.junit.Assert.assertFalse;

 import static org.junit.Assert.assertNotNull;

 import static org.junit.Assert.assertSame;

 import static org.junit.Assert.assertTrue;

@@ -41,10 +43,12 @@
 import java.io.Serializable;

 import java.nio.charset.Charset;

 import java.nio.charset.IllegalCharsetNameException;

+import java.util.ArrayList;

 import java.util.Arrays;

 import java.util.Collection;

 import java.util.Collections;

 import java.util.HashSet;

+import java.util.List;

 import java.util.Set;

 import java.util.Stack;

 import java.util.regex.Matcher;

@@ -714,9 +718,11 @@
         FileInputStream fis = new FileInputStream(f);

         try 

         {

-            if (!f.getName().endsWith(".html"))

+            if (!f.getName().endsWith(".html") 

+                            && !f.getName().endsWith(".csv")

+                            && !f.getName().endsWith(".txt"))

             {

-                // Not HTML

+                // Not HTML or CSV or text

                 return;

             }

             String encoding = System.getProperty("file.encoding", "UTF-8"); //$NON-NLS-1$ //$NON-NLS-2$

@@ -734,6 +740,17 @@
             char cbuf[] = new char[(int)f.length()];

             int l = ir.read(cbuf);

             String s = new String(cbuf, 0, l);

+            // An empty result with a filename ending .csv might be forced into HTML type

+            if (f.getName().endsWith(".csv") && !s.startsWith("<!DOCTYPE HTML PUBLIC"))

+            {

+                checkCSV(f, s);

+                return;

+            }

+            if (f.getName().endsWith(".txt"))

+            {

+                checkTXT(f, s);

+                return;

+            }

 

             /*

              *  All these checks are approximate and would be confused

@@ -741,9 +758,9 @@
              */

 

             // Some basic checks

-            assertThat("Expected charset", s, containsString("content=\"text/html;charset=" + encoding + "\""));

-            assertThat("Possible double escaping <", s, not(containsString("&amp;lt;")));

-            assertThat("Possible double escaping &", s, not(containsString("&amp;amp;")));

+            assertThat(f + " Expected charset", s, containsString("content=\"text/html;charset=" + encoding + "\""));

+            assertThat(f + " Possible double escaping <", s, not(containsString("&amp;lt;")));

+            assertThat(f + " Possible double escaping &", s, not(containsString("&amp;amp;")));

 

             /*

              * Rough test for bad tag - might indicate unescaped '<'.

@@ -947,6 +964,153 @@
         }

     }

 

+    /**

+     * Check a text file generated from a table or tree.

+     */

+    private void checkTXT(File f, String s)

+    {

+        String lines[] = s.split("\r?\n");

+        if (lines.length >= 2 && lines[1].matches("-+"))

+        {

+            // Check the last row is dashes

+            assertThat(f.getPath(), lines[lines.length - 1], equalTo(lines[1]));

+            // Check that no row is longer than the dashes

+            int maxlen = lines[1].length();

+            for (int i = 0; i < lines.length; ++i)

+            {

+                assertThat(f+" "+(i+1)+":"+lines[i], lines[i].length(), lessThanOrEqualTo(maxlen));

+            }

+            // Check the divisions on each line match the header

+            for (int i = 2; i < lines.length - 1; ++i)

+            {

+                // Sometimes the text has a line feed, splitting the file

+                String line = "";

+             l: for (int p = i; p >= 2; --p)

+                {

+                    line = lines[p] + "\n "+ line;

+                    for (int j = lines[0].indexOf('|'); j >= 0; j = lines[0].indexOf('|', j +1))

+                    {

+                        // Try to fix up split lines

+                        if (line.length() < maxlen && (line.length() < j || line.charAt(j) != '|'))

+                            continue l;

+                    }

+                    break;

+                }

+                for (int j = lines[0].indexOf('|'); j >= 0; j = lines[0].indexOf('|', j +1))

+                {

+                    assertThat(f+" "+(i+1)+":"+(j+1)+" "+line, line.length(), greaterThan(j));

+                    assertThat(f+" "+(i+1)+":"+(j+1)+" "+line, line.charAt(j), equalTo('|'));

+                }

+            }

+            if (lines.length > 2)

+            {

+                // check the first row is not blank (empty filter row?)

+                assertFalse(lines[2].matches("[ |]+"));

+            }

+        }

+    }

+

+    /**

+     * Check a CSV (comma separated value) file.

+     */

+    private void checkCSV(File f, String s)

+    {

+        List<List<String>>all = split(f, s);

+        for (List<String> l : all)

+        {

+            assertThat(l.size(), equalTo(all.get(0).size()));

+        }

+    }

+

+    /**

+     * Split a CSV file into lines and fields

+     * @param f

+     * @param s

+     * @return a list of lists of fields

+     */

+    public List<List<String>>split(File f, String s) {

+        List<List<String>>res1 = new ArrayList<List<String>>();

+        List<String>res = new ArrayList<String>();

+        boolean inquote = false;

+        boolean prevquote = false;

+        boolean prevcr = false;

+        StringBuilder sb = new StringBuilder();

+        for (char c : s.toCharArray())

+        {

+            if (inquote)

+            {

+                if (c == '"')

+                {

+                    prevquote = true;

+                    inquote = false;

+                }

+                else

+                {

+                    sb.append(c);

+                    prevquote = false;

+                }

+            } else {

+                switch (c) {

+                    case '"':

+                        assertFalse(f+" "+s, prevcr);

+                        if (prevquote) {

+                            sb.append(c);

+                            inquote = true;

+                            prevquote = false;

+                        } else {

+                            inquote = true;

+                            prevquote = false;

+                        }

+                        break;

+                    case ',':

+                        assertFalse(f+" "+s, prevcr);

+                        res.add(sb.toString());

+                        sb.setLength(0);

+                        break;

+                    case '\r':

+                        assertFalse(f+" "+s, prevcr);

+                        prevcr = true;

+                        break;

+                    case '\n':

+                        assertFalse(f+" "+s, inquote);

+                        res.add(sb.toString());

+                        sb.setLength(0);

+                        res1.add(res);

+                        res = new ArrayList<String>();

+                        break;

+                    default:

+                        assertFalse(f+" "+s, prevcr);

+                        sb.append(c);

+                        break;

+                }

+            }

+        }

+        if (sb.length() > 0)

+            assertThat(f.toString(), sb.toString(), equalTo(""));

+        assertThat(f+" "+res,res.size(), equalTo(0));

+        return res1;

+    }

+

+    @Test

+    public void testAllQueriesReportText() throws SnapshotException, IOException

+    {

+        IProgressListener checkListener = new CheckedProgressListener(collector);

+        SnapshotQuery query = SnapshotQuery.parse("default_report org.eclipse.mat.tests:all -params format=txt", snapshot);

+        IResult t = query.execute(checkListener);

+        assertNotNull(t);

+        checkHTMLResult(t);

+    }

+

+    @Test

+    public void testAllQueriesReportCSV() throws SnapshotException, IOException

+    {

+        IProgressListener checkListener = new CheckedProgressListener(collector);

+        SnapshotQuery query = SnapshotQuery.parse("default_report org.eclipse.mat.tests:all -params format=csv", snapshot);

+        IResult t = query.execute(checkListener);

+        assertNotNull(t);

+        checkHTMLResult(t);

+    }

+

     @Test

     public void listEntries() throws SnapshotException

     {

diff --git a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/Messages.java b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/Messages.java
index 0395b25..26b701f 100644
--- a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/Messages.java
+++ b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/Messages.java
@@ -1,10 +1,10 @@
 /*******************************************************************************

- * Copyright (c) 2010, 2020 SAP AG, IBM Corporation and others.

+ * Copyright (c) 2010, 2021 SAP AG, IBM Corporation and others.

  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License 2.0

  * which accompanies this distribution, and is available at

- * https://www.eclipse.org/legal/epl-2.0/
- *
+ * https://www.eclipse.org/legal/epl-2.0/

+ *

  * SPDX-License-Identifier: EPL-2.0

  *

  * Contributors:

@@ -94,10 +94,12 @@
     public static String ExportActions_Export;

     public static String ExportActions_ExportCSV;

     public static String ExportActions_ExportHTML;

+    public static String ExportActions_ExportTXT;

     public static String ExportActions_ExportToCSV;

     public static String ExportActions_ExportToHTML;

     public static String ExportActions_ExportToTxt;

     public static String ExportActions_PlainText;

+    public static String ExportActions_PlainText2;

     public static String ExportActions_ZippedWebPage;

     public static String FieldsContentProvider_Displayed;

     public static String FileOpenDialogEditor_ChooseFile;

diff --git a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/viewer/ExportActions.java b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/viewer/ExportActions.java
index e4224e7..5c95474 100644
--- a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/viewer/ExportActions.java
+++ b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/viewer/ExportActions.java
@@ -1,14 +1,15 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2010 SAP AG.

+ * Copyright (c) 2008, 2021 SAP AG and IBM Corporation.

  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License 2.0

  * which accompanies this distribution, and is available at

- * https://www.eclipse.org/legal/epl-2.0/
- *
+ * https://www.eclipse.org/legal/epl-2.0/

+ *

  * SPDX-License-Identifier: EPL-2.0

  *

  * Contributors:

  *    SAP AG - initial API and implementation

+ *    Andrew Johnson - export of text using renderer

  *******************************************************************************/

 package org.eclipse.mat.ui.internal.viewer;

 

@@ -17,6 +18,7 @@
 import java.io.IOException;

 import java.io.PrintWriter;

 import java.net.URL;

+import java.util.Arrays;

 import java.util.HashSet;

 import java.util.LinkedList;

 import java.util.Set;

@@ -49,6 +51,8 @@
 import org.eclipse.swt.widgets.FileDialog;

 import org.eclipse.swt.widgets.MessageBox;

 import org.eclipse.swt.widgets.Shell;

+import org.eclipse.swt.widgets.Table;

+import org.eclipse.swt.widgets.TableItem;

 import org.eclipse.swt.widgets.Tree;

 import org.eclipse.swt.widgets.TreeItem;

 

@@ -84,56 +88,7 @@
             final int limit = (controlItem != null && controlItem.getTotals() != null) ? controlItem.getTotals()

                             .getVisibleItems() : 25;

 

-            // extract expanded items

-

-            // problem:

-            // because the result tree can create objects on every #getChilden()

-            // call, objects in the hash map will not match and the expansion

-            // state of nested objects will not be acknowledged

-

-            // the requirement to implement #equals and #hashCode seems too

-            // heavy (and might not help, because one and the same object might

-            // show up multiple times in the tree, but the decision has too be

-            // made structurally)

-            if (control instanceof Tree)

-            {

-                Tree tree = (Tree) control;

-

-                LinkedList<TreeItem> stack = new LinkedList<TreeItem>();

-

-                TreeItem[] items = tree.getItems();

-                for (TreeItem treeItem : items)

-                    stack.add(treeItem);

-

-                final Set<Object> expanded = new HashSet<Object>();

-                while (!stack.isEmpty())

-                {

-                    TreeItem item = stack.removeFirst();

-                    if (item.getExpanded())

-                    {

-                        Object data = item.getData();

-                        if (data != null)

-                            expanded.add(data);

-

-                        items = item.getItems();

-                        for (TreeItem treeItem : items)

-                            stack.add(treeItem);

-                    }

-                }

-

-                result.setSelectionProvider(new ISelectionProvider()

-                {

-                    public boolean isExpanded(Object row)

-                    {

-                        return expanded.contains(row);

-                    }

-

-                    public boolean isSelected(Object row)

-                    {

-                        return false;

-                    }

-                });

-            }

+            expandedTree(control, result);

 

             new Job(Messages.ExportActions_ExportHTML)

             {

@@ -164,6 +119,90 @@
         }

     }

 

+    private static void expandedTree(Control control, RefinedStructuredResult result)

+    {

+        // extract expanded items

+

+        // problem:

+        // because the result tree can create objects on every #getChilden()

+        // call, objects in the hash map will not match and the expansion

+        // state of nested objects will not be acknowledged

+

+        // the requirement to implement #equals and #hashCode seems too

+        // heavy (and might not help, because one and the same object might

+        // show up multiple times in the tree, but the decision has too be

+        // made structurally)

+        if (control instanceof Tree)

+        {

+            Tree tree = (Tree) control;

+

+            LinkedList<TreeItem> stack = new LinkedList<TreeItem>();

+            Set<TreeItem> selects = new HashSet<TreeItem>(Arrays.asList(tree.getSelection()));

+            TreeItem[] items = tree.getItems();

+            for (TreeItem treeItem : items)

+                stack.add(treeItem);

+

+            final Set<Object> expanded = new HashSet<Object>();

+            final Set<Object> selected = new HashSet<Object>();

+            while (!stack.isEmpty())

+            {

+                TreeItem item = stack.removeFirst();

+                if (selects.contains(item))

+                {

+                    Object data = item.getData();

+                    if (data != null)

+                        selected.add(data);

+                }

+                if (item.getExpanded())

+                {

+                    Object data = item.getData();

+                    if (data != null)

+                        expanded.add(data);

+

+                    items = item.getItems();

+                    for (TreeItem treeItem : items)

+                        stack.add(treeItem);

+                }

+            }

+

+            result.setSelectionProvider(new ISelectionProvider()

+            {

+                public boolean isExpanded(Object row)

+                {

+                    return expanded.contains(row);

+                }

+

+                public boolean isSelected(Object row)

+                {

+                    return selected.contains(row);

+                }

+            });

+        }

+        else if (control instanceof Table)

+        {

+            Table table = (Table)control;

+            final Set<Object> selected = new HashSet<Object>();

+            for (TableItem item : table.getSelection())

+            {

+                Object data = item.getData();

+                if (data != null)

+                    selected.add(data);

+            }

+            result.setSelectionProvider(new ISelectionProvider()

+            {

+                public boolean isExpanded(Object row)

+                {

+                    return false;

+                }

+

+                public boolean isSelected(Object row)

+                {

+                    return selected.contains(row);

+                }

+            });

+        }

+    }

+

     /* package */static class CsvExport extends Action

     {

         private Control control;

@@ -227,25 +266,69 @@
     /* package */static class TxtExport extends Action

     {

         private Control control;

+        private IResult result;

+        private IQueryContext queryContext;

 

-        public TxtExport(Control control)

+        public TxtExport(Control control, IResult result, IQueryContext queryContext)

         {

             super(Messages.ExportActions_ExportToTxt, MemoryAnalyserPlugin

                             .getImageDescriptor(MemoryAnalyserPlugin.ISharedImages.EXPORT_TXT));

             this.control = control;

+            this.result = result;

+            this.queryContext = queryContext;

         }

 

         @Override

         public void run()

         {

             ExportDialog dialog = new ExportDialog(control.getShell(), //

-                            new String[] { Messages.ExportActions_PlainText }, //

-                            new String[] { "*.txt" });//$NON-NLS-1$

-            String fileName = dialog.open();

+                            new String[] { Messages.ExportActions_PlainText, Messages.ExportActions_PlainText2 }, //

+                            new String[] { "*.txt", "*.txt" });//$NON-NLS-1$ //$NON-NLS-2$

+            final String fileName = dialog.open();

             if (fileName == null)

                 return;

 

-            Copy.exportToTxtFile(control, fileName);

+            // Old version of export

+            if (dialog.dlg.getFilterIndex() < 1) {

+                Copy.exportToTxtFile(control, fileName);

+                return;

+            }

+

+            if (result instanceof RefinedStructuredResult)

+                expandedTree(control, (RefinedStructuredResult)result);

+

+            new Job(Messages.ExportActions_ExportTXT)

+            {

+

+                @Override

+                protected IStatus run(IProgressMonitor monitor)

+                {

+                    PrintWriter writer = null;

+

+                    try

+                    {

+                        IOutputter outputter = RendererRegistry.instance().match("txt", result.getClass());//$NON-NLS-1$

+                        writer = new PrintWriter(new FileWriter(fileName));

+                        outputter.process(new ContextImpl(queryContext, //

+                                        new File(fileName).getParentFile()), result, writer);

+                        writer.flush();

+                        writer.close();

+                    }

+                    catch (IOException e)

+                    {

+                        return ErrorHelper.createErrorStatus(e);

+                    }

+                    finally

+                    {

+                        if (writer != null)

+                            writer.close();

+                    }

+

+                    return Status.OK_STATUS;

+                }

+

+            }.schedule();

+

         }

     }

 

diff --git a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/viewer/RefinedResultViewer.java b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/viewer/RefinedResultViewer.java
index c4fd7ee..ae7d8ec 100644
--- a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/viewer/RefinedResultViewer.java
+++ b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/viewer/RefinedResultViewer.java
@@ -1,10 +1,10 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2020 SAP AG and IBM Corporation

+ * Copyright (c) 2008, 2021 SAP AG and IBM Corporation

  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License 2.0

  * which accompanies this distribution, and is available at

- * https://www.eclipse.org/legal/epl-2.0/
- *
+ * https://www.eclipse.org/legal/epl-2.0/

+ *

  * SPDX-License-Identifier: EPL-2.0

  *

  * Contributors:

@@ -78,7 +78,6 @@
 import org.eclipse.swt.widgets.MessageBox;

 import org.eclipse.swt.widgets.Text;

 import org.eclipse.swt.widgets.Widget;

-import org.eclipse.ui.PlatformUI;

 import org.eclipse.ui.actions.ActionFactory;

 import org.eclipse.ui.themes.ColorUtil;

 

@@ -761,7 +760,7 @@
             {

                 menu.add(new ExportActions.HtmlExport(control, result, context));

                 menu.add(new ExportActions.CsvExport(control, result, context));

-                menu.add(new ExportActions.TxtExport(control));

+                menu.add(new ExportActions.TxtExport(control, result, context));

             }

         };

 

diff --git a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/messages.properties b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/messages.properties
index 72950fb..e102e82 100644
--- a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/messages.properties
+++ b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/messages.properties
@@ -1,10 +1,10 @@
 ###############################################################################

-# Copyright (c) 2010,2020 SAP AG, IBM Corporation and others.

+# Copyright (c) 2010,2021 SAP AG, IBM Corporation and others.

 # All rights reserved. This program and the accompanying materials

 # are made available under the terms of the Eclipse Public License 2.0

 # which accompanies this distribution, and is available at

-# https://www.eclipse.org/legal/epl-2.0/
-#
+# https://www.eclipse.org/legal/epl-2.0/

+#

 # SPDX-License-Identifier: EPL-2.0

 #

 # Contributors:

@@ -116,10 +116,12 @@
 ExportActions_Export=Export

 ExportActions_ExportCSV=Export CSV

 ExportActions_ExportHTML=Export HTML

+ExportActions_ExportTXT=Export TXT

 ExportActions_ExportToCSV=Export to CSV...

 ExportActions_ExportToHTML=Export to HTML...

 ExportActions_ExportToTxt=Export to TXT...

 ExportActions_PlainText=Plain Text(*.txt)

+ExportActions_PlainText2=Plain Text(*.txt) - new version

 ExportActions_ZippedWebPage=Zipped Web Page (*.zip)

 FieldsContentProvider_Displayed={0} out of {1} displayed

 FileOpenDialogEditor_ChooseFile=Choose File...