package com.jetbrains.lang.dart.ide.errorTreeView;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ui.ColumnInfo;
import com.intellij.util.ui.ListTableModel;
import com.jetbrains.lang.dart.ide.annotator.DartAnnotator;
import icons.DartIcons;
import org.dartlang.analysis.server.protocol.AnalysisError;
import org.dartlang.analysis.server.protocol.AnalysisErrorSeverity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.util.*;
import java.util.List;
class DartProblemsTableModel extends ListTableModel<DartProblem> {
private static final TableCellRenderer MESSAGE_RENDERER = new DefaultTableCellRenderer() {
@Override
public JLabel getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
// Do not emphasize focused cell, drawing the whole row as selected is enough
final JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
final DartProblem problem = (DartProblem)value;
setText(problem.getErrorMessage());
setToolTipText(generateToolTipText(problem.getErrorMessage(), problem.getCorrectionMessage()));
final String severity = problem.getSeverity();
setIcon(AnalysisErrorSeverity.ERROR.equals(severity)
? AllIcons.General.Error
: AnalysisErrorSeverity.WARNING.equals(severity)
? DartIcons.Dart_warning
: AllIcons.General.Information);
return label;
}
};
@NotNull
private static String generateToolTipText(@Nullable final String message, @Nullable final String correction) {
String messageSanitized = StringUtil.notNullize(message).replaceAll("\\\\n", "\n");
String correctionSanitized = StringUtil.notNullize(correction).replaceAll("\\\\n", "\n");
return correctionSanitized.isEmpty() ? messageSanitized : messageSanitized + "\n\n" + correctionSanitized;
}
private static final TableCellRenderer LOCATION_RENDERER = new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
// Do not emphasize focused cell, drawing the whole row as selected is enough
return super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
}
};
private final Project myProject;
@NotNull private final DartProblemsPresentationHelper myPresentationHelper;
// Kind of hack to keep a reference to the live collection used in a super class, but it allows to improve performance greatly.
// Having it in hand we can do bulk rows removal with a single fireTableRowsDeleted() call afterwards
private final List<DartProblem> myItems;
private RowSorter.SortKey mySortKey = new RowSorter.SortKey(1, SortOrder.ASCENDING);
private int myErrorCount = 0;
private int myWarningCount = 0;
private int myHintCount = 0;
private int myErrorCountAfterFilter = 0;
private int myWarningCountAfterFilter = 0;
private int myHintCountAfterFilter = 0;
private final Comparator<DartProblem> myDescriptionComparator = new DartProblemsComparator(DartProblemsComparator.MESSAGE_COLUMN_ID);
private final Comparator<DartProblem> myLocationComparator = new DartProblemsComparator(DartProblemsComparator.LOCATION_COLUMN_ID);
public DartProblemsTableModel(@NotNull final Project project, @NotNull final DartProblemsPresentationHelper presentationHelper) {
myProject = project;
myPresentationHelper = presentationHelper;
myItems = new ArrayList<>();
setColumnInfos(new ColumnInfo[]{createDescriptionColumn(), createLocationColumn()});
setItems(myItems);
setSortable(true);
}
@NotNull
private ColumnInfo<DartProblem, DartProblem> createDescriptionColumn() {
return new ColumnInfo<DartProblem, DartProblem>("Description") {
@Nullable
@Override
public Comparator<DartProblem> getComparator() {
return myDescriptionComparator;
}
@Nullable
@Override
public TableCellRenderer getRenderer(@NotNull final DartProblem problem) {
return MESSAGE_RENDERER;
}
@NotNull
@Override
public DartProblem valueOf(@NotNull final DartProblem problem) {
return problem;
}
};
}
@NotNull
private ColumnInfo<DartProblem, String> createLocationColumn() {
return new ColumnInfo<DartProblem, String>("Location") {
@Nullable
@Override
public Comparator<DartProblem> getComparator() {
return myLocationComparator;
}
@Nullable
@Override
public TableCellRenderer getRenderer(DartProblem problem) {
return LOCATION_RENDERER;
}
@NotNull
@Override
public String valueOf(@NotNull final DartProblem problem) {
return problem.getPresentableLocation();
}
};
}
@Override
public RowSorter.SortKey getDefaultSortKey() {
return mySortKey;
}
@Override
public boolean canExchangeRows(int oldIndex, int newIndex) {
return false;
}
@Override
public void exchangeRows(int idx1, int idx2) {
throw new IllegalStateException();
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return false;
}
public void removeRows(final int firstRow, final int lastRow) {
assert lastRow >= firstRow;
for (int i = lastRow; i >= firstRow; i--) {
final DartProblem removed = myItems.remove(i);
if (AnalysisErrorSeverity.ERROR.equals(removed.getSeverity())) myErrorCount--;
if (AnalysisErrorSeverity.WARNING.equals(removed.getSeverity())) myWarningCount--;
if (AnalysisErrorSeverity.INFO.equals(removed.getSeverity())) myHintCount--;
updateProblemsCountAfterFilter(removed, false);
}
fireTableRowsDeleted(firstRow, lastRow);
}
public void removeAll() {
final int rowCount = getRowCount();
if (rowCount > 0) {
myItems.clear();
fireTableRowsDeleted(0, rowCount - 1);
}
myErrorCount = 0;
myWarningCount = 0;
myHintCount = 0;
myErrorCountAfterFilter = 0;
myWarningCountAfterFilter = 0;
myHintCountAfterFilter = 0;
}
/**
* If <code>selectedProblem</code> was removed and similar one added again then this method returns the added one,
* so that the caller could update selected row in the table
*/
@Nullable
public DartProblem setErrorsAndReturnReplacementForSelection(@NotNull final Map<String, List<AnalysisError>> filePathToErrors,
@Nullable final DartProblem selectedProblem) {
final boolean selectedProblemRemoved = removeRowsForFilesInSet(filePathToErrors.keySet(), selectedProblem);
return addErrorsAndReturnReplacementForSelection(filePathToErrors, selectedProblemRemoved ? selectedProblem : null);
}
private boolean removeRowsForFilesInSet(@NotNull final Set<String> filePaths, @Nullable final DartProblem selectedProblem) {
// Looks for regions in table items that should be removed and removes them.
// For performance reasons we try to call removeRows() as rare as possible, that means with regions as big as possible.
// Logic is based on the fact that all errors for each particular file are stored continuously in the myItems model
boolean selectedProblemRemoved = false;
int matchedFilesCount = 0;
for (int i = getRowCount() - 1; i >= 0; i--) {
final DartProblem problem = getItem(i);
if (filePaths.contains(problem.getSystemIndependentPath())) {
matchedFilesCount++;
final int lastRowToDelete = i;
if (problem == selectedProblem) {
selectedProblemRemoved = true;
}
DartProblem lastProblemForCurrentFile = problem;
int j = i - 1;
while (j >= 0) {
final DartProblem previousProblem = getItem(j);
if (previousProblem.getSystemIndependentPath().equals(lastProblemForCurrentFile.getSystemIndependentPath())) {
// previousProblem should be removed from the table as well
j--;
if (previousProblem == selectedProblem) {
selectedProblemRemoved = true;
}
continue;
}
if (filePaths.contains(previousProblem.getSystemIndependentPath())) {
matchedFilesCount++;
// continue iterating the table because we met a range of problems for another file that also should be removed
lastProblemForCurrentFile = previousProblem;
j--;
if (previousProblem == selectedProblem) {
selectedProblemRemoved = true;
}
continue;
}
break;
}
final int firstRowToDelete = j + 1;
removeRows(firstRowToDelete, lastRowToDelete);
if (matchedFilesCount == filePaths.size()) {
break;
}
//noinspection AssignmentToForLoopParameter
i = j + 1; // rewind according to the amount of removed rows
}
}
return selectedProblemRemoved;
}
@Nullable
private DartProblem addErrorsAndReturnReplacementForSelection(@NotNull final Map<String, List<AnalysisError>> filePathToErrors,
@Nullable final DartProblem oldSelectedProblem) {
DartProblem newSelectedProblem = null;
final List<DartProblem> problemsToAdd = new ArrayList<>();
for (Map.Entry<String, List<AnalysisError>> entry : filePathToErrors.entrySet()) {
final String filePath = entry.getKey();
final VirtualFile vFile = LocalFileSystem.getInstance().findFileByPath(filePath);
final List<AnalysisError> errors = vFile != null && ProjectFileIndex.getInstance(myProject).isInContent(vFile)
? entry.getValue()
: AnalysisError.EMPTY_LIST;
for (AnalysisError analysisError : errors) {
if (DartAnnotator.shouldIgnoreMessageFromDartAnalyzer(filePath, analysisError.getLocation().getFile())) {
continue;
}
final DartProblem problem = new DartProblem(myProject, analysisError);
problemsToAdd.add(problem);
if (oldSelectedProblem != null &&
lookSimilar(problem, oldSelectedProblem) &&
(newSelectedProblem == null ||
// check if current problem is closer to oldSelectedProblem
(Math.abs(oldSelectedProblem.getLineNumber() - newSelectedProblem.getLineNumber()) >=
Math.abs(oldSelectedProblem.getLineNumber() - problem.getLineNumber())))) {
newSelectedProblem = problem;
}
if (AnalysisErrorSeverity.ERROR.equals(problem.getSeverity())) myErrorCount++;
if (AnalysisErrorSeverity.WARNING.equals(problem.getSeverity())) myWarningCount++;
if (AnalysisErrorSeverity.INFO.equals(problem.getSeverity())) myHintCount++;
updateProblemsCountAfterFilter(problem, true);
}
}
if (!problemsToAdd.isEmpty()) {
addRows(problemsToAdd);
}
return newSelectedProblem;
}
private static boolean lookSimilar(@NotNull final DartProblem problem1, @NotNull final DartProblem problem2) {
return problem1.getSeverity().equals(problem2.getSeverity()) &&
problem1.getErrorMessage().equals(problem2.getErrorMessage()) &&
problem1.getSystemIndependentPath().equals(problem2.getSystemIndependentPath());
}
private void updateProblemsCountAfterFilter(@NotNull final DartProblem problem, final boolean incrementNotDecrement) {
if (myPresentationHelper.shouldShowProblem(problem)) {
if (incrementNotDecrement) {
if (AnalysisErrorSeverity.ERROR.equals(problem.getSeverity())) myErrorCountAfterFilter++;
if (AnalysisErrorSeverity.WARNING.equals(problem.getSeverity())) myWarningCountAfterFilter++;
if (AnalysisErrorSeverity.INFO.equals(problem.getSeverity())) myHintCountAfterFilter++;
}
else {
if (AnalysisErrorSeverity.ERROR.equals(problem.getSeverity())) myErrorCountAfterFilter--;
if (AnalysisErrorSeverity.WARNING.equals(problem.getSeverity())) myWarningCountAfterFilter--;
if (AnalysisErrorSeverity.INFO.equals(problem.getSeverity())) myHintCountAfterFilter--;
}
}
}
public void setSortKey(@NotNull final RowSorter.SortKey sortKey) {
mySortKey = sortKey;
}
public void onFilterChanged() {
ApplicationManager.getApplication().assertIsDispatchThread();
if (myPresentationHelper.areFiltersApplied()) {
myErrorCountAfterFilter = 0;
myWarningCountAfterFilter = 0;
myHintCountAfterFilter = 0;
for (DartProblem problem : myItems) {
updateProblemsCountAfterFilter(problem, true);
}
}
else {
myErrorCountAfterFilter = myErrorCount;
myWarningCountAfterFilter = myWarningCount;
myHintCountAfterFilter = myHintCount;
}
}
boolean hasErrors() {
return myErrorCount > 0;
}
boolean hasWarnings() {
return myWarningCount > 0;
}
@NotNull
public String getStatusText() {
final StringBuilder b = new StringBuilder();
final List<String> summary = new ArrayList<>();
if (myPresentationHelper.isShowErrors() && myErrorCountAfterFilter > 0) {
summary.add(myErrorCountAfterFilter + " " + StringUtil.pluralize("error", myErrorCountAfterFilter));
}
if (myPresentationHelper.isShowWarnings() && myWarningCountAfterFilter > 0) {
summary.add(myWarningCountAfterFilter + " " + StringUtil.pluralize("warning", myWarningCountAfterFilter));
}
if (myPresentationHelper.isShowHints() && myHintCountAfterFilter > 0) {
summary.add(myHintCountAfterFilter + " " + StringUtil.pluralize("hint", myHintCountAfterFilter));
}
if (summary.isEmpty()) {
if (myPresentationHelper.areFiltersApplied()) {
return getFilterTypeText();
}
else {
return "";
}
}
if (summary.size() == 2) {
b.append(StringUtil.join(summary, " and "));
}
else {
b.append(StringUtil.join(summary, ", "));
}
if (myPresentationHelper.areFiltersApplied()) {
b.append(" (");
b.append(getFilterTypeText());
b.append(")");
}
return b.toString();
}
private String getFilterTypeText() {
final StringBuilder builder = new StringBuilder();
switch (myPresentationHelper.getFileFilterMode()) {
case All:
break;
case ContentRoot:
builder.append("filtering by current content root");
break;
case DartPackage:
builder.append("filtering by current Dart package");
break;
case Directory:
builder.append("filtering by current directory");
break;
case File:
builder.append("filtering by current file");
break;
}
if (!myPresentationHelper.isShowErrors() || !myPresentationHelper.isShowWarnings() || !myPresentationHelper.isShowHints()) {
builder.append(builder.length() == 0 ? "filtering by severity" : " and severity");
}
return builder.toString();
}
private class DartProblemsComparator implements Comparator<DartProblem> {
private static final int MESSAGE_COLUMN_ID = 0;
private static final int LOCATION_COLUMN_ID = 1;
private final int myColumn;
DartProblemsComparator(final int column) {
myColumn = column;
}
@Override
public int compare(@NotNull final DartProblem problem1, @NotNull final DartProblem problem2) {
if (myPresentationHelper.isGroupBySeverity()) {
final int s1 = getSeverityIndex(problem1);
final int s2 = getSeverityIndex(problem2);
if (s1 != s2) {
// Regardless of sorting direction, if 'Group by severity' is selected then we should keep errors on top
return mySortKey.getSortOrder() == SortOrder.ASCENDING ? s1 - s2 : s2 - s1;
}
}
if (myColumn == MESSAGE_COLUMN_ID) {
return StringUtil.compare(problem1.getErrorMessage(), problem2.getErrorMessage(), false);
}
if (myColumn == LOCATION_COLUMN_ID) {
final int result = StringUtil.compare(problem1.getPresentableLocationWithoutLineNumber(),
problem2.getPresentableLocationWithoutLineNumber(), false);
if (result != 0) {
return result;
}
else {
// Regardless of sorting direction, line numbers within the same file should be sorted in ascending order
return mySortKey.getSortOrder() == SortOrder.ASCENDING
? problem1.getLineNumber() - problem2.getLineNumber()
: problem2.getLineNumber() - problem1.getLineNumber();
}
}
return 0;
}
private int getSeverityIndex(@NotNull final DartProblem problem) {
final String severity = problem.getSeverity();
if (AnalysisErrorSeverity.ERROR.equals(severity)) {
return 0;
}
if (AnalysisErrorSeverity.WARNING.equals(severity)) {
return 1;
}
return 2;
}
}
}