IDEA-185851 Populate classes on typing of filter expression
[idea/community.git] / java / debugger / impl / src / com / intellij / debugger / memory / ui / ClassesTable.java
1 // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.debugger.memory.ui;
3
4 import com.intellij.debugger.memory.component.InstancesTracker;
5 import com.intellij.debugger.memory.tracking.TrackerForNewInstances;
6 import com.intellij.debugger.memory.tracking.TrackingType;
7 import com.intellij.debugger.memory.utils.AbstractTableColumnDescriptor;
8 import com.intellij.debugger.memory.utils.AbstractTableModelWithColumns;
9 import com.intellij.debugger.memory.utils.InstancesProvider;
10 import com.intellij.icons.AllIcons;
11 import com.intellij.openapi.Disposable;
12 import com.intellij.openapi.actionSystem.DataKey;
13 import com.intellij.openapi.actionSystem.DataProvider;
14 import com.intellij.openapi.application.ApplicationManager;
15 import com.intellij.openapi.util.TextRange;
16 import com.intellij.psi.codeStyle.MinusculeMatcher;
17 import com.intellij.psi.codeStyle.NameUtil;
18 import com.intellij.ui.ColoredTableCellRenderer;
19 import com.intellij.ui.JBColor;
20 import com.intellij.ui.SimpleTextAttributes;
21 import com.intellij.ui.speedSearch.SpeedSearchUtil;
22 import com.intellij.ui.table.JBTable;
23 import com.intellij.util.containers.FList;
24 import com.intellij.util.ui.JBDimension;
25 import com.intellij.util.ui.JBUI;
26 import com.sun.jdi.ObjectReference;
27 import com.sun.jdi.ReferenceType;
28 import org.jetbrains.annotations.NonNls;
29 import org.jetbrains.annotations.NotNull;
30 import org.jetbrains.annotations.Nullable;
31
32 import javax.swing.*;
33 import javax.swing.border.Border;
34 import javax.swing.table.TableColumn;
35 import javax.swing.table.TableColumnModel;
36 import javax.swing.table.TableRowSorter;
37 import java.awt.*;
38 import java.awt.event.MouseAdapter;
39 import java.awt.event.MouseEvent;
40 import java.awt.event.MouseListener;
41 import java.util.*;
42 import java.util.List;
43 import java.util.concurrent.ConcurrentHashMap;
44
45 public class ClassesTable extends JBTable implements DataProvider, Disposable {
46   public static final DataKey<ReferenceType> SELECTED_CLASS_KEY = DataKey.create("ClassesTable.SelectedClass");
47   public static final DataKey<InstancesProvider> NEW_INSTANCES_PROVIDER_KEY =
48     DataKey.create("ClassesTable.NewInstances");
49   public static final DataKey<ReferenceCountProvider> REF_COUNT_PROVIDER_KEY =
50     DataKey.create("ClassesTable.ReferenceCountProvider");
51
52   private static final Border EMPTY_BORDER = BorderFactory.createEmptyBorder();
53   private static final JBColor CLICKABLE_COLOR = new JBColor(new Color(250, 251, 252), new Color(62, 66, 69));
54   private static final String DEFAULT_EMPTY_TEXT = "Nothing to show";
55
56   private static final int CLASSES_COLUMN_PREFERRED_WIDTH = 250;
57   private static final int COUNT_COLUMN_MIN_WIDTH = 80;
58   private static final int DIFF_COLUMN_MIN_WIDTH = 80;
59   private static final UnknownDiffValue UNKNOWN_VALUE = new UnknownDiffValue();
60
61   private final DiffViewTableModel myModel = new DiffViewTableModel();
62   private final Map<ReferenceType, DiffValue> myCounts = new ConcurrentHashMap<>();
63   private final InstancesTracker myInstancesTracker;
64   private final ClassesFilteredView myParent;
65   private final ReferenceCountProvider myCountProvider;
66
67   private boolean myOnlyWithDiff;
68   private boolean myOnlyTracked;
69   private boolean myOnlyWithInstances;
70   private MinusculeMatcher myMatcher = NameUtil.buildMatcher("*").build();
71   private String myFilteringPattern = "";
72
73   private volatile List<ReferenceType> myItems = Collections.unmodifiableList(new ArrayList<>());
74   private boolean myIsShowCounts = true;
75   private MouseListener myMouseListener = null;
76
77   public ClassesTable(@NotNull InstancesTracker tracker, @NotNull ClassesFilteredView parent, boolean onlyWithDiff,
78                       boolean onlyWithInstances,
79                       boolean onlyTracked) {
80     setModel(myModel);
81
82     myOnlyWithDiff = onlyWithDiff;
83     myOnlyWithInstances = onlyWithInstances;
84     myOnlyTracked = onlyTracked;
85     myInstancesTracker = tracker;
86     myParent = parent;
87
88     final TableColumnModel columnModel = getColumnModel();
89     TableColumn classesColumn = columnModel.getColumn(DiffViewTableModel.CLASSNAME_COLUMN_INDEX);
90     TableColumn countColumn = columnModel.getColumn(DiffViewTableModel.COUNT_COLUMN_INDEX);
91     TableColumn diffColumn = columnModel.getColumn(DiffViewTableModel.DIFF_COLUMN_INDEX);
92
93     setAutoResizeMode(AUTO_RESIZE_SUBSEQUENT_COLUMNS);
94     classesColumn.setPreferredWidth(JBUI.scale(CLASSES_COLUMN_PREFERRED_WIDTH));
95
96     countColumn.setMinWidth(JBUI.scale(COUNT_COLUMN_MIN_WIDTH));
97
98     diffColumn.setMinWidth(JBUI.scale(DIFF_COLUMN_MIN_WIDTH));
99
100     setShowGrid(false);
101     setIntercellSpacing(new JBDimension(0, 0));
102
103     setDefaultRenderer(ReferenceType.class, new MyClassColumnRenderer());
104     setDefaultRenderer(Long.class, new MyCountColumnRenderer());
105     setDefaultRenderer(DiffValue.class, new MyDiffColumnRenderer());
106
107     TableRowSorter<DiffViewTableModel> sorter = new TableRowSorter<>(myModel);
108     sorter.setRowFilter(new RowFilter<DiffViewTableModel, Integer>() {
109       @Override
110       public boolean include(Entry<? extends DiffViewTableModel, ? extends Integer> entry) {
111         int ix = entry.getIdentifier();
112         ReferenceType ref = myItems.get(ix);
113         DiffValue diff = myCounts.getOrDefault(ref, UNKNOWN_VALUE);
114
115         boolean isFilteringOptionsRefused = myOnlyWithDiff && diff.diff() == 0
116                                             || myOnlyWithInstances && !diff.hasInstance()
117                                             || myOnlyTracked && myParent.getStrategy(ref) == null;
118         return !(isFilteringOptionsRefused) && myMatcher.matches(ref.name());
119       }
120     });
121
122     List<RowSorter.SortKey> myDefaultSortingKeys = Arrays.asList(
123       new RowSorter.SortKey(DiffViewTableModel.DIFF_COLUMN_INDEX, SortOrder.DESCENDING),
124       new RowSorter.SortKey(DiffViewTableModel.COUNT_COLUMN_INDEX, SortOrder.DESCENDING),
125       new RowSorter.SortKey(DiffViewTableModel.CLASSNAME_COLUMN_INDEX, SortOrder.ASCENDING)
126     );
127     sorter.setSortKeys(myDefaultSortingKeys);
128     setRowSorter(sorter);
129     setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
130
131     myCountProvider = new ReferenceCountProvider() {
132       @Override
133       public int getTotalCount(@NotNull ReferenceType ref) {
134         return (int)myCounts.get(ref).myCurrentCount;
135       }
136
137       @Override
138       public int getDiffCount(@NotNull ReferenceType ref) {
139         return (int)myCounts.get(ref).diff();
140       }
141
142       @Override
143       public int getNewInstancesCount(@NotNull ReferenceType ref) {
144         TrackerForNewInstances strategy = myParent.getStrategy(ref);
145         return strategy == null || !strategy.isReady() ? -1 : strategy.getCount();
146       }
147     };
148   }
149
150   public interface ReferenceCountProvider {
151
152     int getTotalCount(@NotNull ReferenceType ref);
153
154     int getDiffCount(@NotNull ReferenceType ref);
155
156     int getNewInstancesCount(@NotNull ReferenceType ref);
157   }
158
159   @Nullable
160   ReferenceType getSelectedClass() {
161     int selectedRow = getSelectedRow();
162     if (selectedRow != -1) {
163       int ix = convertRowIndexToModel(selectedRow);
164       return myItems.get(ix);
165     }
166
167     return null;
168   }
169
170   @Nullable
171   ReferenceType getClassByName(@NotNull String name) {
172     for (ReferenceType ref : myItems) {
173       if (name.equals(ref.name())) {
174         return ref;
175       }
176     }
177
178     return null;
179   }
180
181   boolean isInClickableMode() {
182     return myMouseListener != null;
183   }
184
185   void makeClickable(@NotNull String text, @NotNull Runnable onClick) {
186     releaseMouseListener();
187     getEmptyText().setText(text);
188
189     if (!ApplicationManager.getApplication().isUnitTestMode() && getMousePosition() != null) {
190       setBackground(CLICKABLE_COLOR);
191     }
192
193     myMouseListener = new MouseAdapter() {
194       @Override
195       public void mouseClicked(MouseEvent e) {
196         onClick.run();
197         releaseMouseListener();
198       }
199
200       @Override
201       public void mouseEntered(MouseEvent e) {
202         setBackground(CLICKABLE_COLOR);
203       }
204
205       @Override
206       public void mouseExited(MouseEvent e) {
207         setBackground(JBColor.background());
208       }
209     };
210
211     addMouseListener(myMouseListener);
212     setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
213   }
214
215   void exitClickableMode() {
216     releaseMouseListener();
217     getEmptyText().setText(DEFAULT_EMPTY_TEXT);
218   }
219
220   private void releaseMouseListener() {
221     ApplicationManager.getApplication().assertIsDispatchThread();
222     if (isInClickableMode()) {
223       removeMouseListener(myMouseListener);
224       myMouseListener = null;
225       setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
226       setBackground(JBColor.background());
227     }
228   }
229
230   void setBusy(boolean value) {
231     setPaintBusy(value);
232   }
233
234   void setFilterPattern(String pattern) {
235     if (!myFilteringPattern.equals(pattern)) {
236       myFilteringPattern = pattern;
237       myMatcher = NameUtil.buildMatcher("*" + pattern).build();
238       fireTableDataChanged();
239       if (getSelectedClass() == null && getRowCount() > 0) {
240         getSelectionModel().setSelectionInterval(0, 0);
241       }
242     }
243   }
244
245   void setFilteringByInstanceExists(boolean value) {
246     if (value != myOnlyWithInstances) {
247       myOnlyWithInstances = value;
248       fireTableDataChanged();
249     }
250   }
251
252   void setFilteringByDiffNonZero(boolean value) {
253     if (myOnlyWithDiff != value) {
254       myOnlyWithDiff = value;
255       fireTableDataChanged();
256     }
257   }
258
259   void setFilteringByTrackingState(boolean value) {
260     if (myOnlyTracked != value) {
261       myOnlyTracked = value;
262       fireTableDataChanged();
263     }
264   }
265
266   public void updateClassesOnly(@NotNull List<ReferenceType> classes) {
267     myIsShowCounts = false;
268     final LinkedHashMap<ReferenceType, Long> class2Count = new LinkedHashMap<>();
269     classes.forEach(x -> class2Count.put(x, 0L));
270     updateCountsInternal(class2Count);
271   }
272
273   public void updateContent(@NotNull Map<ReferenceType, Long> class2Count) {
274     myIsShowCounts = true;
275     updateCountsInternal(class2Count);
276   }
277
278   void hideContent(@NotNull String emptyText) {
279     releaseMouseListener();
280     getEmptyText().setText(emptyText);
281
282     myModel.hide();
283   }
284
285   private void showContent() {
286     myModel.show();
287   }
288
289   private void updateCountsInternal(@NotNull Map<ReferenceType, Long> class2Count) {
290     releaseMouseListener();
291     getEmptyText().setText(DEFAULT_EMPTY_TEXT);
292
293     final ReferenceType selectedClass = myModel.getSelectedClassBeforeHide();
294     int newSelectedIndex = -1;
295     final boolean isInitialized = !myItems.isEmpty();
296     myItems = Collections.unmodifiableList(new ArrayList<>(class2Count.keySet()));
297
298     int i = 0;
299     for (final ReferenceType ref : class2Count.keySet()) {
300       if (ref.equals(selectedClass)) {
301         newSelectedIndex = i;
302       }
303
304       final DiffValue oldValue = isInitialized && !myCounts.containsKey(ref)
305                                  ? new DiffValue(0, 0)
306                                  : myCounts.getOrDefault(ref, UNKNOWN_VALUE);
307       myCounts.put(ref, oldValue.update(class2Count.get(ref)));
308
309       i++;
310     }
311
312     showContent();
313
314     if (newSelectedIndex != -1 && !myModel.isHidden()) {
315       final int ix = convertRowIndexToView(newSelectedIndex);
316       changeSelection(ix,
317                       DiffViewTableModel.CLASSNAME_COLUMN_INDEX, false, false);
318     }
319
320     fireTableDataChanged();
321   }
322
323   @Nullable
324   @Override
325   public Object getData(@NonNls String dataId) {
326     if (SELECTED_CLASS_KEY.is(dataId)) {
327       return getSelectedClass();
328     }
329     if (NEW_INSTANCES_PROVIDER_KEY.is(dataId)) {
330       ReferenceType selectedClass = getSelectedClass();
331       if (selectedClass != null) {
332         TrackerForNewInstances strategy = myParent.getStrategy(selectedClass);
333         if (strategy != null && strategy.isReady()) {
334           List<ObjectReference> newInstances = strategy.getNewInstances();
335           return (InstancesProvider)limit -> newInstances;
336         }
337       }
338     }
339
340     if (REF_COUNT_PROVIDER_KEY.is(dataId)) {
341       return myCountProvider;
342     }
343
344     return null;
345   }
346
347   public void clean(@NotNull String emptyText) {
348     clearSelection();
349     releaseMouseListener();
350     getEmptyText().setText(emptyText);
351     myItems = Collections.emptyList();
352     myCounts.clear();
353     myModel.mySelectedClassWhenHidden = null;
354     fireTableDataChanged();
355   }
356
357   @Override
358   public void dispose() {
359     ApplicationManager.getApplication().invokeLater(() -> clean(""));
360   }
361
362   @Nullable
363   private TrackingType getTrackingType(int row) {
364     ReferenceType ref = (ReferenceType)getValueAt(row, convertColumnIndexToView(DiffViewTableModel.CLASSNAME_COLUMN_INDEX));
365     return myInstancesTracker.getTrackingType(ref.name());
366   }
367
368   private void fireTableDataChanged() {
369     myModel.fireTableDataChanged();
370   }
371
372   class DiffViewTableModel extends AbstractTableModelWithColumns {
373     final static int CLASSNAME_COLUMN_INDEX = 0;
374     final static int COUNT_COLUMN_INDEX = 1;
375     final static int DIFF_COLUMN_INDEX = 2;
376
377     // Workaround: save selection after content of classes table has been hided
378     private ReferenceType mySelectedClassWhenHidden = null;
379     private boolean myIsWithContent = false;
380
381     DiffViewTableModel() {
382       super(new AbstractTableColumnDescriptor[]{
383         new AbstractTableColumnDescriptor("Class", ReferenceType.class) {
384           @Override
385           public Object getValue(int ix) {
386             return myItems.get(ix);
387           }
388         },
389         new AbstractTableColumnDescriptor("Count", Long.class) {
390           @Override
391           public Object getValue(int ix) {
392             return myCounts.getOrDefault(myItems.get(ix), UNKNOWN_VALUE).myCurrentCount;
393           }
394         },
395         new AbstractTableColumnDescriptor("Diff", DiffValue.class) {
396           @Override
397           public Object getValue(int ix) {
398             return myCounts.getOrDefault(myItems.get(ix), UNKNOWN_VALUE);
399           }
400         }
401       });
402     }
403
404     ReferenceType getSelectedClassBeforeHide() {
405       return mySelectedClassWhenHidden;
406     }
407
408     void hide() {
409       if (myIsWithContent) {
410         mySelectedClassWhenHidden = getSelectedClass();
411         myIsWithContent = false;
412         clearSelection();
413         fireTableDataChanged();
414       }
415     }
416
417     void show() {
418       if (!myIsWithContent) {
419         myIsWithContent = true;
420         fireTableDataChanged();
421       }
422     }
423
424     boolean isHidden() {
425       return !myIsWithContent;
426     }
427
428     @Override
429     public int getRowCount() {
430       return myIsWithContent ? myItems.size() : 0;
431     }
432   }
433
434   /**
435    * State transmissions for DiffValue and UnknownDiffValue
436    * unknown -> diff
437    * diff -> diff
438    * <p>
439    * State descriptions:
440    * Unknown - instances count never executed
441    * Diff - actual value
442    */
443   private static class UnknownDiffValue extends DiffValue {
444     UnknownDiffValue() {
445       super(-1);
446     }
447
448     @Override
449     boolean hasInstance() {
450       return true;
451     }
452
453     @Override
454     DiffValue update(long count) {
455       return new DiffValue(count);
456     }
457   }
458
459   private static class DiffValue implements Comparable<DiffValue> {
460     private long myOldCount;
461
462     private long myCurrentCount;
463
464     DiffValue(long count) {
465       this(count, count);
466     }
467
468     DiffValue(long old, long current) {
469       myCurrentCount = current;
470       myOldCount = old;
471     }
472
473     DiffValue update(long count) {
474       myOldCount = myCurrentCount;
475       myCurrentCount = count;
476       return this;
477     }
478
479     boolean hasInstance() {
480       return myCurrentCount > 0;
481     }
482
483     long diff() {
484       return myCurrentCount - myOldCount;
485     }
486
487     @Override
488     public int compareTo(@NotNull DiffValue o) {
489       return Long.compare(diff(), o.diff());
490     }
491   }
492
493   private abstract static class MyTableCellRenderer extends ColoredTableCellRenderer {
494     @Override
495     protected void customizeCellRenderer(JTable table, @Nullable Object value, boolean isSelected,
496                                          boolean hasFocus, int row, int column) {
497
498       if (hasFocus) {
499         setBorder(EMPTY_BORDER);
500       }
501
502       if (value != null) {
503         addText(value, isSelected, row);
504       }
505     }
506
507     protected abstract void addText(@NotNull Object value, boolean isSelected, int row);
508   }
509
510   private class MyClassColumnRenderer extends MyTableCellRenderer {
511     @Override
512     protected void addText(@NotNull Object value, boolean isSelected,
513                            int row) {
514       String presentation = ((ReferenceType)value).name();
515       append(" ");
516       if (isSelected) {
517         FList<TextRange> textRanges = myMatcher.matchingFragments(presentation);
518         if (textRanges != null) {
519           SimpleTextAttributes attributes = new SimpleTextAttributes(getBackground(), getForeground(), null,
520                                                                      SimpleTextAttributes.STYLE_SEARCH_MATCH);
521           SpeedSearchUtil.appendColoredFragments(this, presentation, textRanges,
522                                                  SimpleTextAttributes.REGULAR_ATTRIBUTES, attributes);
523         }
524       }
525       else {
526         append(String.format("%s", presentation), SimpleTextAttributes.REGULAR_ATTRIBUTES);
527       }
528     }
529   }
530
531   private abstract class MyNumericRenderer extends MyTableCellRenderer {
532     @Override
533     protected void addText(@NotNull Object value, boolean isSelected, int row) {
534       if (myIsShowCounts) {
535         setTextAlign(SwingConstants.RIGHT);
536         appendText(value, row);
537       }
538     }
539
540     abstract void appendText(@NotNull Object value, int row);
541   }
542
543   private class MyCountColumnRenderer extends MyNumericRenderer {
544     @Override
545     void appendText(@NotNull Object value, int row) {
546       append(value.toString());
547     }
548   }
549
550   private class MyDiffColumnRenderer extends MyNumericRenderer {
551     private final SimpleTextAttributes myClickableCellAttributes =
552       new SimpleTextAttributes(SimpleTextAttributes.STYLE_UNDERLINE, JBColor.BLUE);
553
554     @Override
555     void appendText(@NotNull Object value, int row) {
556       TrackingType trackingType = getTrackingType(row);
557       if (trackingType != null) {
558         setIcon(AllIcons.Debugger.MemoryView.ClassTracked);
559         setTransparentIconBackground(true);
560       }
561
562       ReferenceType ref = myItems.get(convertRowIndexToModel(row));
563
564       long diff = myCountProvider.getDiffCount(ref);
565       String text = String.format("%s%d", diff > 0 ? "+" : "", diff);
566
567       int newInstancesCount = myCountProvider.getNewInstancesCount(ref);
568       if (newInstancesCount >= 0) {
569         if (newInstancesCount == diff) {
570           append(text, diff == 0 ? SimpleTextAttributes.REGULAR_ATTRIBUTES : myClickableCellAttributes);
571         }
572         else {
573           append(text, SimpleTextAttributes.REGULAR_ATTRIBUTES);
574           if (newInstancesCount != 0) {
575             append(String.format(" (%d)", newInstancesCount), myClickableCellAttributes);
576           }
577         }
578       }
579       else {
580         append(text, SimpleTextAttributes.REGULAR_ATTRIBUTES);
581       }
582     }
583   }
584 }